mirror of
https://github.com/samanhappy/mcphub.git
synced 2025-12-23 18:29:21 -05:00
feat: integrate offcial mcp server registry (#374)
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"semi": false,
|
"semi": true,
|
||||||
"trailingComma": "all",
|
"trailingComma": "all",
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"printWidth": 100,
|
"printWidth": 100,
|
||||||
|
|||||||
@@ -1,51 +1,52 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Server } from '@/types'
|
import { Server } from '@/types';
|
||||||
import { apiPut } from '../utils/fetchInterceptor'
|
import { apiPut } from '../utils/fetchInterceptor';
|
||||||
import ServerForm from './ServerForm'
|
import ServerForm from './ServerForm';
|
||||||
|
|
||||||
interface EditServerFormProps {
|
interface EditServerFormProps {
|
||||||
server: Server
|
server: Server;
|
||||||
onEdit: () => void
|
onEdit: () => void;
|
||||||
onCancel: () => void
|
onCancel: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const EditServerForm = ({ server, onEdit, onCancel }: EditServerFormProps) => {
|
const EditServerForm = ({ server, onEdit, onCancel }: EditServerFormProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation();
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const handleSubmit = async (payload: any) => {
|
const handleSubmit = async (payload: any) => {
|
||||||
try {
|
try {
|
||||||
setError(null)
|
setError(null);
|
||||||
const result = await apiPut(`/servers/${server.name}`, payload)
|
const encodedServerName = encodeURIComponent(server.name);
|
||||||
|
const result = await apiPut(`/servers/${encodedServerName}`, payload);
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
// Use specific error message from the response if available
|
// Use specific error message from the response if available
|
||||||
if (result && result.message) {
|
if (result && result.message) {
|
||||||
setError(result.message)
|
setError(result.message);
|
||||||
} else {
|
} else {
|
||||||
setError(t('server.updateError', { serverName: server.name }))
|
setError(t('server.updateError', { serverName: server.name }));
|
||||||
}
|
}
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
onEdit()
|
onEdit();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error updating server:', err)
|
console.error('Error updating server:', err);
|
||||||
|
|
||||||
// Use friendly error messages based on error type
|
// Use friendly error messages based on error type
|
||||||
if (!navigator.onLine) {
|
if (!navigator.onLine) {
|
||||||
setError(t('errors.network'))
|
setError(t('errors.network'));
|
||||||
} else if (err instanceof TypeError && (
|
} else if (
|
||||||
err.message.includes('NetworkError') ||
|
err instanceof TypeError &&
|
||||||
err.message.includes('Failed to fetch')
|
(err.message.includes('NetworkError') || err.message.includes('Failed to fetch'))
|
||||||
)) {
|
) {
|
||||||
setError(t('errors.serverConnection'))
|
setError(t('errors.serverConnection'));
|
||||||
} else {
|
} else {
|
||||||
setError(t('errors.serverUpdate', { serverName: server.name }))
|
setError(t('errors.serverUpdate', { serverName: server.name }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
|
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
|
||||||
@@ -57,7 +58,7 @@ const EditServerForm = ({ server, onEdit, onCancel }: EditServerFormProps) => {
|
|||||||
formError={error}
|
formError={error}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default EditServerForm
|
export default EditServerForm;
|
||||||
|
|||||||
205
frontend/src/components/RegistryServerCard.tsx
Normal file
205
frontend/src/components/RegistryServerCard.tsx
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { RegistryServerEntry } from '@/types';
|
||||||
|
|
||||||
|
interface RegistryServerCardProps {
|
||||||
|
serverEntry: RegistryServerEntry;
|
||||||
|
onClick: (serverEntry: RegistryServerEntry) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RegistryServerCard: React.FC<RegistryServerCardProps> = ({ serverEntry, onClick }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { server, _meta } = serverEntry;
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
onClick(serverEntry);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get display description
|
||||||
|
const getDisplayDescription = () => {
|
||||||
|
if (server.description && server.description.length <= 150) {
|
||||||
|
return server.description;
|
||||||
|
}
|
||||||
|
return server.description
|
||||||
|
? server.description.slice(0, 150) + '...'
|
||||||
|
: t('registry.noDescription');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Format date for display
|
||||||
|
const formatDate = (dateString?: string) => {
|
||||||
|
if (!dateString) return '';
|
||||||
|
try {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||||
|
const day = date.getDate().toString().padStart(2, '0');
|
||||||
|
return `${year}/${month}/${day}`;
|
||||||
|
} catch {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get icon to display
|
||||||
|
const getIcon = () => {
|
||||||
|
if (server.icons && server.icons.length > 0) {
|
||||||
|
// Prefer light theme icon
|
||||||
|
const lightIcon = server.icons.find((icon) => !icon.theme || icon.theme === 'light');
|
||||||
|
return lightIcon || server.icons[0];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const icon = getIcon();
|
||||||
|
const officialMeta = _meta?.['io.modelcontextprotocol.registry/official'];
|
||||||
|
const isLatest = officialMeta?.isLatest;
|
||||||
|
const publishedAt = officialMeta?.publishedAt;
|
||||||
|
const updatedAt = officialMeta?.updatedAt;
|
||||||
|
|
||||||
|
// Count packages and remotes
|
||||||
|
const packageCount = server.packages?.length || 0;
|
||||||
|
const remoteCount = server.remotes?.length || 0;
|
||||||
|
const totalOptions = packageCount + remoteCount;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="bg-white border border-gray-200 rounded-xl p-4 hover:shadow-lg hover:border-blue-400 hover:-translate-y-1 transition-all duration-300 cursor-pointer group relative overflow-hidden h-full flex flex-col"
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
{/* Background gradient overlay on hover */}
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-br from-blue-50/0 to-purple-50/0 group-hover:from-blue-50/30 group-hover:to-purple-50/30 transition-all duration-300 pointer-events-none" />
|
||||||
|
|
||||||
|
{/* Server Header */}
|
||||||
|
<div className="relative z-10 flex-1 flex flex-col">
|
||||||
|
<div className="flex items-start justify-between mb-3">
|
||||||
|
<div className="flex items-start space-x-3 flex-1">
|
||||||
|
{/* Icon */}
|
||||||
|
{icon ? (
|
||||||
|
<img
|
||||||
|
src={icon.src}
|
||||||
|
alt={server.title}
|
||||||
|
className="w-12 h-12 rounded-lg object-cover flex-shrink-0"
|
||||||
|
onError={(e) => {
|
||||||
|
e.currentTarget.style.display = 'none';
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="w-12 h-12 bg-gradient-to-br from-blue-500 to-purple-600 rounded-lg flex items-center justify-center text-white text-xl font-semibold flex-shrink-0">
|
||||||
|
M
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Title and badges */}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<h3 className="text-lg font-bold text-gray-900 group-hover:text-blue-600 transition-colors duration-200 mb-1 line-clamp-2">
|
||||||
|
{server.name}
|
||||||
|
</h3>
|
||||||
|
<div className="flex flex-wrap gap-1 mb-1">
|
||||||
|
{isLatest && (
|
||||||
|
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
||||||
|
{t('registry.latest')}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
||||||
|
v{server.version}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Server Name */}
|
||||||
|
{/* <div className="mb-2">
|
||||||
|
<p className="text-xs text-gray-500 font-mono">{server.name}</p>
|
||||||
|
</div> */}
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<div className="mb-3 flex-1">
|
||||||
|
<p className="text-gray-600 text-sm leading-relaxed line-clamp-3">
|
||||||
|
{getDisplayDescription()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Installation Options Info */}
|
||||||
|
{totalOptions > 0 && (
|
||||||
|
<div className="mb-3">
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
{packageCount > 0 && (
|
||||||
|
<div className="flex items-center space-x-1">
|
||||||
|
<svg
|
||||||
|
className="w-4 h-4 text-gray-500"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span className="text-sm text-gray-600">
|
||||||
|
{packageCount}{' '}
|
||||||
|
{packageCount === 1 ? t('registry.package') : t('registry.packages')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{remoteCount > 0 && (
|
||||||
|
<div className="flex items-center space-x-1">
|
||||||
|
<svg
|
||||||
|
className="w-4 h-4 text-gray-500"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span className="text-sm text-gray-600">
|
||||||
|
{remoteCount} {remoteCount === 1 ? t('registry.remote') : t('registry.remotes')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Footer - fixed at bottom */}
|
||||||
|
<div className="flex items-center justify-between pt-3 border-t border-gray-100 mt-auto">
|
||||||
|
<div className="flex items-center space-x-2 text-xs text-gray-500">
|
||||||
|
{(publishedAt || updatedAt) && (
|
||||||
|
<>
|
||||||
|
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span>{formatDate(updatedAt || publishedAt)}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center text-blue-600 text-sm font-medium group-hover:text-blue-700 transition-colors">
|
||||||
|
<span>{t('registry.viewDetails')}</span>
|
||||||
|
<svg
|
||||||
|
className="w-4 h-4 ml-1 transform group-hover:translate-x-1 transition-transform"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RegistryServerCard;
|
||||||
698
frontend/src/components/RegistryServerDetail.tsx
Normal file
698
frontend/src/components/RegistryServerDetail.tsx
Normal file
@@ -0,0 +1,698 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import {
|
||||||
|
RegistryServerEntry,
|
||||||
|
RegistryPackage,
|
||||||
|
RegistryRemote,
|
||||||
|
RegistryServerData,
|
||||||
|
ServerConfig,
|
||||||
|
} from '@/types';
|
||||||
|
import ServerForm from './ServerForm';
|
||||||
|
|
||||||
|
interface RegistryServerDetailProps {
|
||||||
|
serverEntry: RegistryServerEntry;
|
||||||
|
onBack: () => void;
|
||||||
|
onInstall?: (server: RegistryServerData, config: ServerConfig) => void;
|
||||||
|
installing?: boolean;
|
||||||
|
isInstalled?: boolean;
|
||||||
|
fetchVersions?: (serverName: string) => Promise<RegistryServerEntry[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RegistryServerDetail: React.FC<RegistryServerDetailProps> = ({
|
||||||
|
serverEntry,
|
||||||
|
onBack,
|
||||||
|
onInstall,
|
||||||
|
installing = false,
|
||||||
|
isInstalled = false,
|
||||||
|
fetchVersions,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { server, _meta } = serverEntry;
|
||||||
|
|
||||||
|
const [_selectedVersion, _setSelectedVersion] = useState<string>(server.version);
|
||||||
|
const [_availableVersions, setAvailableVersions] = useState<RegistryServerEntry[]>([]);
|
||||||
|
const [_loadingVersions, setLoadingVersions] = useState(false);
|
||||||
|
const [modalVisible, setModalVisible] = useState(false);
|
||||||
|
const [selectedInstallType, setSelectedInstallType] = useState<'package' | 'remote' | null>(null);
|
||||||
|
const [selectedOption, setSelectedOption] = useState<RegistryPackage | RegistryRemote | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
const [installError, setInstallError] = useState<string | null>(null);
|
||||||
|
const [expandedSections, setExpandedSections] = useState<Record<string, boolean>>({
|
||||||
|
packages: true,
|
||||||
|
remotes: true,
|
||||||
|
repository: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const officialMeta = _meta?.['io.modelcontextprotocol.registry/official'];
|
||||||
|
|
||||||
|
// Load available versions
|
||||||
|
useEffect(() => {
|
||||||
|
const loadVersions = async () => {
|
||||||
|
if (fetchVersions) {
|
||||||
|
setLoadingVersions(true);
|
||||||
|
try {
|
||||||
|
const versions = await fetchVersions(server.name);
|
||||||
|
setAvailableVersions(versions);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load versions:', error);
|
||||||
|
} finally {
|
||||||
|
setLoadingVersions(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadVersions();
|
||||||
|
}, [server.name, fetchVersions]);
|
||||||
|
|
||||||
|
// Get icon to display
|
||||||
|
const getIcon = () => {
|
||||||
|
if (server.icons && server.icons.length > 0) {
|
||||||
|
const lightIcon = server.icons.find((icon) => !icon.theme || icon.theme === 'light');
|
||||||
|
return lightIcon || server.icons[0];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const icon = getIcon();
|
||||||
|
|
||||||
|
// Format date
|
||||||
|
const formatDate = (dateString?: string) => {
|
||||||
|
if (!dateString) return '';
|
||||||
|
try {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date.toLocaleDateString();
|
||||||
|
} catch {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Toggle section expansion
|
||||||
|
const toggleSection = (section: string) => {
|
||||||
|
setExpandedSections((prev) => ({ ...prev, [section]: !prev[section] }));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle install button click
|
||||||
|
const handleInstallClick = (
|
||||||
|
type: 'package' | 'remote',
|
||||||
|
option: RegistryPackage | RegistryRemote,
|
||||||
|
) => {
|
||||||
|
setSelectedInstallType(type);
|
||||||
|
setSelectedOption(option);
|
||||||
|
setInstallError(null);
|
||||||
|
setModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle modal close
|
||||||
|
const handleModalClose = () => {
|
||||||
|
setModalVisible(false);
|
||||||
|
setInstallError(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle install submission
|
||||||
|
const handleInstallSubmit = async (payload: any) => {
|
||||||
|
try {
|
||||||
|
if (!onInstall || !selectedOption || !selectedInstallType) return;
|
||||||
|
|
||||||
|
setInstallError(null);
|
||||||
|
|
||||||
|
// Extract the ServerConfig from the payload
|
||||||
|
const config: ServerConfig = payload.config;
|
||||||
|
|
||||||
|
// Call onInstall with server data and config
|
||||||
|
onInstall(server, config);
|
||||||
|
setModalVisible(false);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error installing server:', err);
|
||||||
|
setInstallError(t('errors.serverInstall'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Build initial data for ServerForm
|
||||||
|
const getInitialFormData = () => {
|
||||||
|
if (!selectedOption || !selectedInstallType) return null;
|
||||||
|
console.log('Building initial form data for:', selectedOption);
|
||||||
|
|
||||||
|
if (selectedInstallType === 'package' && 'identifier' in selectedOption) {
|
||||||
|
const pkg = selectedOption as RegistryPackage;
|
||||||
|
|
||||||
|
// Build environment variables from package definition
|
||||||
|
const env: Record<string, string> = {};
|
||||||
|
if (pkg.environmentVariables) {
|
||||||
|
pkg.environmentVariables.forEach((envVar) => {
|
||||||
|
env[envVar.name] = envVar.default || '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const command = getCommand(pkg.registryType);
|
||||||
|
return {
|
||||||
|
name: server.name,
|
||||||
|
status: 'disconnected' as const,
|
||||||
|
config: {
|
||||||
|
type: 'stdio' as const,
|
||||||
|
command: command,
|
||||||
|
args: getArgs(command, pkg),
|
||||||
|
env: Object.keys(env).length > 0 ? env : undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else if (selectedInstallType === 'remote' && 'url' in selectedOption) {
|
||||||
|
const remote = selectedOption as RegistryRemote;
|
||||||
|
|
||||||
|
// Build headers from remote definition
|
||||||
|
const headers: Record<string, string> = {};
|
||||||
|
if (remote.headers) {
|
||||||
|
remote.headers.forEach((header) => {
|
||||||
|
headers[header.name] = header.default || header.value || '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine transport type - default to streamable-http for remotes
|
||||||
|
const transportType = remote.type === 'sse' ? ('sse' as const) : ('streamable-http' as const);
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: server.name,
|
||||||
|
status: 'disconnected' as const,
|
||||||
|
config: {
|
||||||
|
type: transportType,
|
||||||
|
url: remote.url,
|
||||||
|
headers: Object.keys(headers).length > 0 ? headers : undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Render package option
|
||||||
|
const renderPackage = (pkg: RegistryPackage, index: number) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="border border-gray-200 rounded-lg p-4 mb-3 hover:border-blue-400 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between mb-3">
|
||||||
|
<div className="flex-1">
|
||||||
|
<h4 className="font-medium text-gray-900">{pkg.identifier}</h4>
|
||||||
|
{pkg.version && <p className="text-sm text-gray-500">Version: {pkg.version}</p>}
|
||||||
|
{pkg.runtimeHint && <p className="text-sm text-gray-600 mt-1">{pkg.runtimeHint}</p>}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => handleInstallClick('package', pkg)}
|
||||||
|
disabled={isInstalled || installing}
|
||||||
|
className={`px-4 py-2 rounded text-sm font-medium transition-colors ${
|
||||||
|
isInstalled
|
||||||
|
? 'bg-green-600 text-white cursor-default'
|
||||||
|
: installing
|
||||||
|
? 'bg-gray-400 text-white cursor-not-allowed'
|
||||||
|
: 'bg-blue-600 text-white hover:bg-blue-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isInstalled
|
||||||
|
? t('registry.installed')
|
||||||
|
: installing
|
||||||
|
? t('registry.installing')
|
||||||
|
: t('registry.install')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Package details */}
|
||||||
|
{pkg.registryType && (
|
||||||
|
<div className="text-sm text-gray-600 mb-2">
|
||||||
|
<span className="font-medium">Registry:</span> {pkg.registryType}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Transport type */}
|
||||||
|
{pkg.transport && (
|
||||||
|
<div className="text-sm text-gray-600 mb-2">
|
||||||
|
<span className="font-medium">Transport:</span> {pkg.transport.type}
|
||||||
|
{pkg.transport.url && <span className="ml-2 text-gray-500">({pkg.transport.url})</span>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Environment Variables */}
|
||||||
|
{pkg.environmentVariables && pkg.environmentVariables.length > 0 && (
|
||||||
|
<div className="mt-3 border-t border-gray-200 pt-3">
|
||||||
|
<h5 className="text-sm font-medium text-gray-700 mb-2">
|
||||||
|
{t('registry.environmentVariables')}:
|
||||||
|
</h5>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{pkg.environmentVariables.map((envVar, envIndex) => (
|
||||||
|
<div key={envIndex} className="text-sm">
|
||||||
|
<div className="flex items-start">
|
||||||
|
<span className="font-mono text-gray-900 font-medium">{envVar.name}</span>
|
||||||
|
{envVar.isRequired && (
|
||||||
|
<span className="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-red-100 text-red-800">
|
||||||
|
{t('common.required')}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{envVar.isSecret && (
|
||||||
|
<span className="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-yellow-100 text-yellow-800">
|
||||||
|
{t('common.secret')}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{envVar.description && <p className="text-gray-600 mt-1">{envVar.description}</p>}
|
||||||
|
{envVar.default && (
|
||||||
|
<p className="text-gray-500 mt-1">
|
||||||
|
<span className="font-medium">{t('common.default')}:</span>{' '}
|
||||||
|
<span className="font-mono">{envVar.default}</span>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Package Arguments */}
|
||||||
|
{pkg.packageArguments && pkg.packageArguments.length > 0 && (
|
||||||
|
<div className="mt-3 border-t border-gray-200 pt-3">
|
||||||
|
<h5 className="text-sm font-medium text-gray-700 mb-2">
|
||||||
|
{t('registry.packageArguments')}:
|
||||||
|
</h5>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{pkg.packageArguments.map((arg, argIndex) => (
|
||||||
|
<div key={argIndex} className="text-sm">
|
||||||
|
<div className="flex items-start">
|
||||||
|
<span className="font-mono text-gray-900 font-medium">{arg.name}</span>
|
||||||
|
{arg.isRequired && (
|
||||||
|
<span className="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-red-100 text-red-800">
|
||||||
|
{t('common.required')}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{arg.isSecret && (
|
||||||
|
<span className="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-yellow-100 text-yellow-800">
|
||||||
|
{t('common.secret')}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{arg.isRepeated && (
|
||||||
|
<span className="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-purple-100 text-purple-800">
|
||||||
|
{t('common.repeated')}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{arg.description && <p className="text-gray-600 mt-1">{arg.description}</p>}
|
||||||
|
{arg.type && (
|
||||||
|
<p className="text-gray-500 mt-1">
|
||||||
|
<span className="font-medium">{t('common.type')}:</span>{' '}
|
||||||
|
<span className="font-mono">{arg.type}</span>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{arg.default && (
|
||||||
|
<p className="text-gray-500 mt-1">
|
||||||
|
<span className="font-medium">{t('common.default')}:</span>{' '}
|
||||||
|
<span className="font-mono">{arg.default}</span>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{arg.value && (
|
||||||
|
<p className="text-gray-500 mt-1">
|
||||||
|
<span className="font-medium">{t('common.value')}:</span>{' '}
|
||||||
|
<span className="font-mono">{arg.value}</span>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{arg.valueHint && (
|
||||||
|
<p className="text-gray-500 mt-1">
|
||||||
|
<span className="font-medium">{t('common.valueHint')}:</span>{' '}
|
||||||
|
<span className="font-mono">{arg.valueHint}</span>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{arg.choices && arg.choices.length > 0 && (
|
||||||
|
<p className="text-gray-500 mt-1">
|
||||||
|
<span className="font-medium">{t('common.choices')}:</span>{' '}
|
||||||
|
<span className="font-mono">{arg.choices.join(', ')}</span>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Render remote option
|
||||||
|
const renderRemote = (remote: RegistryRemote, index: number) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="border border-gray-200 rounded-lg p-4 mb-3 hover:border-blue-400 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between mb-3">
|
||||||
|
<div className="flex-1">
|
||||||
|
<h4 className="font-medium text-gray-900">{remote.type}</h4>
|
||||||
|
<p className="text-sm text-gray-600 mt-1 break-all">{remote.url}</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => handleInstallClick('remote', remote)}
|
||||||
|
disabled={isInstalled || installing}
|
||||||
|
className={`px-4 py-2 rounded text-sm font-medium transition-colors ${
|
||||||
|
isInstalled
|
||||||
|
? 'bg-green-600 text-white cursor-default'
|
||||||
|
: installing
|
||||||
|
? 'bg-gray-400 text-white cursor-not-allowed'
|
||||||
|
: 'bg-blue-600 text-white hover:bg-blue-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isInstalled
|
||||||
|
? t('registry.installed')
|
||||||
|
: installing
|
||||||
|
? t('registry.installing')
|
||||||
|
: t('registry.install')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Headers */}
|
||||||
|
{remote.headers && remote.headers.length > 0 && (
|
||||||
|
<div className="mt-3 border-t border-gray-200 pt-3">
|
||||||
|
<h5 className="text-sm font-medium text-gray-700 mb-2">{t('registry.headers')}:</h5>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{remote.headers.map((header, headerIndex) => (
|
||||||
|
<div key={headerIndex} className="text-sm">
|
||||||
|
<div className="flex items-start">
|
||||||
|
<span className="font-mono text-gray-900 font-medium">{header.name}</span>
|
||||||
|
{header.isRequired && (
|
||||||
|
<span className="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-red-100 text-red-800">
|
||||||
|
{t('common.required')}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{header.isSecret && (
|
||||||
|
<span className="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-yellow-100 text-yellow-800">
|
||||||
|
{t('common.secret')}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{header.description && <p className="text-gray-600 mt-1">{header.description}</p>}
|
||||||
|
{header.value && (
|
||||||
|
<p className="text-gray-500 mt-1">
|
||||||
|
<span className="font-medium">{t('common.value')}:</span>{' '}
|
||||||
|
<span className="font-mono">{header.value}</span>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{header.default && (
|
||||||
|
<p className="text-gray-500 mt-1">
|
||||||
|
<span className="font-medium">{t('common.default')}:</span>{' '}
|
||||||
|
<span className="font-mono">{header.default}</span>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white shadow rounded-lg p-6">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<button
|
||||||
|
onClick={onBack}
|
||||||
|
className="flex items-center text-blue-600 hover:text-blue-800 mb-4 transition-colors"
|
||||||
|
>
|
||||||
|
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M15 19l-7-7 7-7"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{t('registry.backToList')}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="flex items-start space-x-4">
|
||||||
|
{/* Icon */}
|
||||||
|
{icon ? (
|
||||||
|
<img
|
||||||
|
src={icon.src}
|
||||||
|
alt={server.title}
|
||||||
|
className="w-20 h-20 rounded-lg object-cover flex-shrink-0"
|
||||||
|
onError={(e) => {
|
||||||
|
e.currentTarget.style.display = 'none';
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="w-20 h-20 bg-gradient-to-br from-blue-500 to-purple-600 rounded-lg flex items-center justify-center text-white text-3xl font-semibold flex-shrink-0">
|
||||||
|
M
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Title and metadata */}
|
||||||
|
<div className="flex-1">
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900 mb-2">{server.name}</h1>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap gap-2 mb-3">
|
||||||
|
{officialMeta?.isLatest && (
|
||||||
|
<span className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-green-100 text-green-800">
|
||||||
|
{t('registry.latest')}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-100 text-blue-800">
|
||||||
|
v{server.version}
|
||||||
|
</span>
|
||||||
|
{officialMeta?.status && (
|
||||||
|
<span className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-gray-100 text-gray-800">
|
||||||
|
{officialMeta.status}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{/* Dates */}
|
||||||
|
<span className="flex flex-wrap items-center gap-4 text-sm text-gray-600">
|
||||||
|
{officialMeta?.publishedAt && (
|
||||||
|
<div>
|
||||||
|
<span className="font-medium">{t('registry.published')}:</span>{' '}
|
||||||
|
{formatDate(officialMeta.publishedAt)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{officialMeta?.updatedAt && (
|
||||||
|
<div>
|
||||||
|
<span className="font-medium">{t('registry.updated')}:</span>{' '}
|
||||||
|
{formatDate(officialMeta.updatedAt)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<h2 className="text-xl font-semibold text-gray-900 mb-3">{t('registry.description')}</h2>
|
||||||
|
<p className="text-gray-700 leading-relaxed whitespace-pre-wrap">{server.description}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Website */}
|
||||||
|
{server.websiteUrl && (
|
||||||
|
<div className="mb-6">
|
||||||
|
<h2 className="text-xl font-semibold text-gray-900 mb-3">{t('registry.website')}</h2>
|
||||||
|
<a
|
||||||
|
href={server.websiteUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-blue-600 hover:text-blue-800 hover:underline"
|
||||||
|
>
|
||||||
|
{server.websiteUrl}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Packages */}
|
||||||
|
{server.packages && server.packages.length > 0 && (
|
||||||
|
<div className="mb-6">
|
||||||
|
<button
|
||||||
|
onClick={() => toggleSection('packages')}
|
||||||
|
className="flex items-center justify-between w-full text-xl font-semibold text-gray-900 mb-3 hover:text-blue-600 transition-colors"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{t('registry.packages')} ({server.packages.length})
|
||||||
|
</span>
|
||||||
|
<svg
|
||||||
|
className={`w-5 h-5 transform transition-transform ${expandedSections.packages ? 'rotate-180' : ''}`}
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M19 9l-7 7-7-7"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
{expandedSections.packages && (
|
||||||
|
<div className="space-y-3">{server.packages.map(renderPackage)}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Remotes */}
|
||||||
|
{server.remotes && server.remotes.length > 0 && (
|
||||||
|
<div className="mb-6">
|
||||||
|
<button
|
||||||
|
onClick={() => toggleSection('remotes')}
|
||||||
|
className="flex items-center justify-between w-full text-xl font-semibold text-gray-900 mb-3 hover:text-blue-600 transition-colors"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{t('registry.remotes')} ({server.remotes.length})
|
||||||
|
</span>
|
||||||
|
<svg
|
||||||
|
className={`w-5 h-5 transform transition-transform ${expandedSections.remotes ? 'rotate-180' : ''}`}
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M19 9l-7 7-7-7"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
{expandedSections.remotes && (
|
||||||
|
<div className="space-y-3">{server.remotes.map(renderRemote)}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Repository */}
|
||||||
|
{server.repository && (
|
||||||
|
<div className="mb-6">
|
||||||
|
<button
|
||||||
|
onClick={() => toggleSection('repository')}
|
||||||
|
className="flex items-center justify-between w-full text-xl font-semibold text-gray-900 mb-3 hover:text-blue-600 transition-colors"
|
||||||
|
>
|
||||||
|
<span>{t('registry.repository')}</span>
|
||||||
|
<svg
|
||||||
|
className={`w-5 h-5 transform transition-transform ${expandedSections.repository ? 'rotate-180' : ''}`}
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M19 9l-7 7-7-7"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
{expandedSections.repository && (
|
||||||
|
<div className="border border-gray-200 rounded-lg p-4">
|
||||||
|
{server.repository.url && (
|
||||||
|
<div className="mb-2">
|
||||||
|
<span className="font-medium text-gray-700">URL:</span>{' '}
|
||||||
|
<a
|
||||||
|
href={server.repository.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-blue-600 hover:text-blue-800 hover:underline break-all"
|
||||||
|
>
|
||||||
|
{server.repository.url}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{server.repository.source && (
|
||||||
|
<div className="mb-2">
|
||||||
|
<span className="font-medium text-gray-700">Source:</span>{' '}
|
||||||
|
{server.repository.source}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{server.repository.subfolder && (
|
||||||
|
<div className="mb-2">
|
||||||
|
<span className="font-medium text-gray-700">Subfolder:</span>{' '}
|
||||||
|
{server.repository.subfolder}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{server.repository.id && (
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-gray-700">ID:</span> {server.repository.id}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Install Modal */}
|
||||||
|
{modalVisible && selectedOption && selectedInstallType && (
|
||||||
|
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
|
||||||
|
<ServerForm
|
||||||
|
onSubmit={handleInstallSubmit}
|
||||||
|
onCancel={handleModalClose}
|
||||||
|
modalTitle={t('registry.installServer', { name: server.title || server.name })}
|
||||||
|
formError={installError}
|
||||||
|
initialData={getInitialFormData()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RegistryServerDetail;
|
||||||
|
// Helper function to determine command based on registry type
|
||||||
|
function getCommand(registryType: string): string {
|
||||||
|
// Map registry types to appropriate commands
|
||||||
|
switch (registryType.toLowerCase()) {
|
||||||
|
case 'pypi':
|
||||||
|
case 'python':
|
||||||
|
return 'uvx';
|
||||||
|
case 'npm':
|
||||||
|
case 'node':
|
||||||
|
return 'npx';
|
||||||
|
case 'oci':
|
||||||
|
case 'docker':
|
||||||
|
return 'docker';
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to get appropriate args based on command type and package identifier
|
||||||
|
function getArgs(command: string, pkg: RegistryPackage): string[] {
|
||||||
|
const identifier = [pkg.identifier + (pkg.version ? `@${pkg.version}` : '')];
|
||||||
|
|
||||||
|
// Build package arguments if available
|
||||||
|
const packageArgs: string[] = [];
|
||||||
|
if (pkg.packageArguments && pkg.packageArguments.length > 0) {
|
||||||
|
pkg.packageArguments.forEach((arg) => {
|
||||||
|
// Add required arguments or arguments with default values
|
||||||
|
if (arg.isRequired || arg.default || arg.value) {
|
||||||
|
const argName = `--${arg.name}`;
|
||||||
|
// Priority: value > default > placeholder
|
||||||
|
const argValue = arg.value || arg.default || `\${${arg.name.toUpperCase()}}`;
|
||||||
|
packageArgs.push(argName, argValue);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map commands to appropriate argument patterns
|
||||||
|
switch (command.toLowerCase()) {
|
||||||
|
case 'uvx':
|
||||||
|
// For Python packages: uvx package-name --arg1 value1 --arg2 value2
|
||||||
|
return [...identifier, ...packageArgs];
|
||||||
|
case 'npx':
|
||||||
|
// For Node.js packages: npx package-name --arg1 value1 --arg2 value2
|
||||||
|
return [...identifier, ...packageArgs];
|
||||||
|
case 'docker': {
|
||||||
|
// add envs from environment variables if available
|
||||||
|
const envs: string[] = [];
|
||||||
|
if (pkg.environmentVariables) {
|
||||||
|
pkg.environmentVariables.forEach((env) => {
|
||||||
|
envs.push('-e', `${env.name}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// For Docker images: docker run -i package-name --arg1 value1 --arg2 value2
|
||||||
|
return ['run', '-i', '--rm', ...envs, ...identifier, ...packageArgs];
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// If no specific pattern is defined, return identifier with package args
|
||||||
|
return [...identifier, ...packageArgs];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +1,23 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Server, EnvVar, ServerFormData } from '@/types'
|
import { Server, EnvVar, ServerFormData } from '@/types';
|
||||||
|
|
||||||
interface ServerFormProps {
|
interface ServerFormProps {
|
||||||
onSubmit: (payload: any) => void
|
onSubmit: (payload: any) => void;
|
||||||
onCancel: () => void
|
onCancel: () => void;
|
||||||
initialData?: Server | null
|
initialData?: Server | null;
|
||||||
modalTitle: string
|
modalTitle: string;
|
||||||
formError?: string | null
|
formError?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formError = null }: ServerFormProps) => {
|
const ServerForm = ({
|
||||||
const { t } = useTranslation()
|
onSubmit,
|
||||||
|
onCancel,
|
||||||
|
initialData = null,
|
||||||
|
modalTitle,
|
||||||
|
formError = null,
|
||||||
|
}: ServerFormProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
// Determine the initial server type from the initialData
|
// Determine the initial server type from the initialData
|
||||||
const getInitialServerType = () => {
|
const getInitialServerType = () => {
|
||||||
@@ -26,7 +32,19 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const [serverType, setServerType] = useState<'stdio' | 'sse' | 'streamable-http' | 'openapi'>(getInitialServerType());
|
const getInitialServerEnvVars = (data: Server | null): EnvVar[] => {
|
||||||
|
if (!data || !data.config || !data.config.env) return [];
|
||||||
|
|
||||||
|
return Object.entries(data.config.env).map(([key, value]) => ({
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
description: '', // You can set a default description if needed
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const [serverType, setServerType] = useState<'stdio' | 'sse' | 'streamable-http' | 'openapi'>(
|
||||||
|
getInitialServerType(),
|
||||||
|
);
|
||||||
|
|
||||||
const [formData, setFormData] = useState<ServerFormData>({
|
const [formData, setFormData] = useState<ServerFormData>({
|
||||||
name: (initialData && initialData.name) || '',
|
name: (initialData && initialData.name) || '',
|
||||||
@@ -40,149 +58,178 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
|||||||
: '',
|
: '',
|
||||||
args: (initialData && initialData.config && initialData.config.args) || [],
|
args: (initialData && initialData.config && initialData.config.args) || [],
|
||||||
type: getInitialServerType(), // Initialize the type field
|
type: getInitialServerType(), // Initialize the type field
|
||||||
env: [],
|
env: getInitialServerEnvVars(initialData),
|
||||||
headers: [],
|
headers: [],
|
||||||
options: {
|
options: {
|
||||||
timeout: (initialData && initialData.config && initialData.config.options && initialData.config.options.timeout) || 60000,
|
timeout:
|
||||||
resetTimeoutOnProgress: (initialData && initialData.config && initialData.config.options && initialData.config.options.resetTimeoutOnProgress) || false,
|
(initialData &&
|
||||||
maxTotalTimeout: (initialData && initialData.config && initialData.config.options && initialData.config.options.maxTotalTimeout) || undefined,
|
initialData.config &&
|
||||||
|
initialData.config.options &&
|
||||||
|
initialData.config.options.timeout) ||
|
||||||
|
60000,
|
||||||
|
resetTimeoutOnProgress:
|
||||||
|
(initialData &&
|
||||||
|
initialData.config &&
|
||||||
|
initialData.config.options &&
|
||||||
|
initialData.config.options.resetTimeoutOnProgress) ||
|
||||||
|
false,
|
||||||
|
maxTotalTimeout:
|
||||||
|
(initialData &&
|
||||||
|
initialData.config &&
|
||||||
|
initialData.config.options &&
|
||||||
|
initialData.config.options.maxTotalTimeout) ||
|
||||||
|
undefined,
|
||||||
},
|
},
|
||||||
// OpenAPI configuration initialization
|
// OpenAPI configuration initialization
|
||||||
openapi: initialData && initialData.config && initialData.config.openapi ? {
|
openapi:
|
||||||
url: initialData.config.openapi.url || '',
|
initialData && initialData.config && initialData.config.openapi
|
||||||
schema: initialData.config.openapi.schema ? JSON.stringify(initialData.config.openapi.schema, null, 2) : '',
|
? {
|
||||||
inputMode: initialData.config.openapi.url ? 'url' : (initialData.config.openapi.schema ? 'schema' : 'url'),
|
url: initialData.config.openapi.url || '',
|
||||||
version: initialData.config.openapi.version || '3.1.0',
|
schema: initialData.config.openapi.schema
|
||||||
securityType: initialData.config.openapi.security?.type || 'none',
|
? JSON.stringify(initialData.config.openapi.schema, null, 2)
|
||||||
// API Key initialization
|
: '',
|
||||||
apiKeyName: initialData.config.openapi.security?.apiKey?.name || '',
|
inputMode: initialData.config.openapi.url
|
||||||
apiKeyIn: initialData.config.openapi.security?.apiKey?.in || 'header',
|
? 'url'
|
||||||
apiKeyValue: initialData.config.openapi.security?.apiKey?.value || '',
|
: initialData.config.openapi.schema
|
||||||
// HTTP auth initialization
|
? 'schema'
|
||||||
httpScheme: initialData.config.openapi.security?.http?.scheme || 'bearer',
|
: 'url',
|
||||||
httpCredentials: initialData.config.openapi.security?.http?.credentials || '',
|
version: initialData.config.openapi.version || '3.1.0',
|
||||||
// OAuth2 initialization
|
securityType: initialData.config.openapi.security?.type || 'none',
|
||||||
oauth2Token: initialData.config.openapi.security?.oauth2?.token || '',
|
// API Key initialization
|
||||||
// OpenID Connect initialization
|
apiKeyName: initialData.config.openapi.security?.apiKey?.name || '',
|
||||||
openIdConnectUrl: initialData.config.openapi.security?.openIdConnect?.url || '',
|
apiKeyIn: initialData.config.openapi.security?.apiKey?.in || 'header',
|
||||||
openIdConnectToken: initialData.config.openapi.security?.openIdConnect?.token || '',
|
apiKeyValue: initialData.config.openapi.security?.apiKey?.value || '',
|
||||||
// Passthrough headers initialization
|
// HTTP auth initialization
|
||||||
passthroughHeaders: initialData.config.openapi.passthroughHeaders ? initialData.config.openapi.passthroughHeaders.join(', ') : '',
|
httpScheme: initialData.config.openapi.security?.http?.scheme || 'bearer',
|
||||||
} : {
|
httpCredentials: initialData.config.openapi.security?.http?.credentials || '',
|
||||||
inputMode: 'url',
|
// OAuth2 initialization
|
||||||
url: '',
|
oauth2Token: initialData.config.openapi.security?.oauth2?.token || '',
|
||||||
schema: '',
|
// OpenID Connect initialization
|
||||||
version: '3.1.0',
|
openIdConnectUrl: initialData.config.openapi.security?.openIdConnect?.url || '',
|
||||||
securityType: 'none',
|
openIdConnectToken: initialData.config.openapi.security?.openIdConnect?.token || '',
|
||||||
passthroughHeaders: '',
|
// Passthrough headers initialization
|
||||||
}
|
passthroughHeaders: initialData.config.openapi.passthroughHeaders
|
||||||
})
|
? initialData.config.openapi.passthroughHeaders.join(', ')
|
||||||
|
: '',
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
inputMode: 'url',
|
||||||
|
url: '',
|
||||||
|
schema: '',
|
||||||
|
version: '3.1.0',
|
||||||
|
securityType: 'none',
|
||||||
|
passthroughHeaders: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const [envVars, setEnvVars] = useState<EnvVar[]>(
|
const [envVars, setEnvVars] = useState<EnvVar[]>(
|
||||||
initialData && initialData.config && initialData.config.env
|
initialData && initialData.config && initialData.config.env
|
||||||
? Object.entries(initialData.config.env).map(([key, value]) => ({ key, value }))
|
? Object.entries(initialData.config.env).map(([key, value]) => ({ key, value }))
|
||||||
: [],
|
: [],
|
||||||
)
|
);
|
||||||
|
|
||||||
const [headerVars, setHeaderVars] = useState<EnvVar[]>(
|
const [headerVars, setHeaderVars] = useState<EnvVar[]>(
|
||||||
initialData && initialData.config && initialData.config.headers
|
initialData && initialData.config && initialData.config.headers
|
||||||
? Object.entries(initialData.config.headers).map(([key, value]) => ({ key, value }))
|
? Object.entries(initialData.config.headers).map(([key, value]) => ({ key, value }))
|
||||||
: [],
|
: [],
|
||||||
)
|
);
|
||||||
|
|
||||||
const [isRequestOptionsExpanded, setIsRequestOptionsExpanded] = useState<boolean>(false)
|
const [isRequestOptionsExpanded, setIsRequestOptionsExpanded] = useState<boolean>(false);
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null);
|
||||||
const isEdit = !!initialData
|
const isEdit = !!initialData;
|
||||||
|
|
||||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const { name, value } = e.target
|
const { name, value } = e.target;
|
||||||
setFormData({ ...formData, [name]: value })
|
setFormData({ ...formData, [name]: value });
|
||||||
}
|
};
|
||||||
|
|
||||||
// Transform space-separated arguments string into array
|
// Transform space-separated arguments string into array
|
||||||
const handleArgsChange = (value: string) => {
|
const handleArgsChange = (value: string) => {
|
||||||
const args = value.split(' ').filter((arg) => arg.trim() !== '')
|
const args = value.split(' ').filter((arg) => arg.trim() !== '');
|
||||||
setFormData({ ...formData, arguments: value, args })
|
setFormData({ ...formData, arguments: value, args });
|
||||||
}
|
};
|
||||||
|
|
||||||
const updateServerType = (type: 'stdio' | 'sse' | 'streamable-http' | 'openapi') => {
|
const updateServerType = (type: 'stdio' | 'sse' | 'streamable-http' | 'openapi') => {
|
||||||
setServerType(type);
|
setServerType(type);
|
||||||
setFormData(prev => ({ ...prev, type }));
|
setFormData((prev) => ({ ...prev, type }));
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleEnvVarChange = (index: number, field: 'key' | 'value', value: string) => {
|
const handleEnvVarChange = (index: number, field: 'key' | 'value', value: string) => {
|
||||||
const newEnvVars = [...envVars]
|
const newEnvVars = [...envVars];
|
||||||
newEnvVars[index][field] = value
|
newEnvVars[index][field] = value;
|
||||||
setEnvVars(newEnvVars)
|
setEnvVars(newEnvVars);
|
||||||
}
|
};
|
||||||
|
|
||||||
const addEnvVar = () => {
|
const addEnvVar = () => {
|
||||||
setEnvVars([...envVars, { key: '', value: '' }])
|
setEnvVars([...envVars, { key: '', value: '' }]);
|
||||||
}
|
};
|
||||||
|
|
||||||
const removeEnvVar = (index: number) => {
|
const removeEnvVar = (index: number) => {
|
||||||
const newEnvVars = [...envVars]
|
const newEnvVars = [...envVars];
|
||||||
newEnvVars.splice(index, 1)
|
newEnvVars.splice(index, 1);
|
||||||
setEnvVars(newEnvVars)
|
setEnvVars(newEnvVars);
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleHeaderVarChange = (index: number, field: 'key' | 'value', value: string) => {
|
const handleHeaderVarChange = (index: number, field: 'key' | 'value', value: string) => {
|
||||||
const newHeaderVars = [...headerVars]
|
const newHeaderVars = [...headerVars];
|
||||||
newHeaderVars[index][field] = value
|
newHeaderVars[index][field] = value;
|
||||||
setHeaderVars(newHeaderVars)
|
setHeaderVars(newHeaderVars);
|
||||||
}
|
};
|
||||||
|
|
||||||
const addHeaderVar = () => {
|
const addHeaderVar = () => {
|
||||||
setHeaderVars([...headerVars, { key: '', value: '' }])
|
setHeaderVars([...headerVars, { key: '', value: '' }]);
|
||||||
}
|
};
|
||||||
|
|
||||||
const removeHeaderVar = (index: number) => {
|
const removeHeaderVar = (index: number) => {
|
||||||
const newHeaderVars = [...headerVars]
|
const newHeaderVars = [...headerVars];
|
||||||
newHeaderVars.splice(index, 1)
|
newHeaderVars.splice(index, 1);
|
||||||
setHeaderVars(newHeaderVars)
|
setHeaderVars(newHeaderVars);
|
||||||
}
|
};
|
||||||
|
|
||||||
// Handle options changes
|
// Handle options changes
|
||||||
const handleOptionsChange = (field: 'timeout' | 'resetTimeoutOnProgress' | 'maxTotalTimeout', value: number | boolean | undefined) => {
|
const handleOptionsChange = (
|
||||||
setFormData(prev => ({
|
field: 'timeout' | 'resetTimeoutOnProgress' | 'maxTotalTimeout',
|
||||||
|
value: number | boolean | undefined,
|
||||||
|
) => {
|
||||||
|
setFormData((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
options: {
|
options: {
|
||||||
...prev.options,
|
...prev.options,
|
||||||
[field]: value
|
[field]: value,
|
||||||
}
|
},
|
||||||
}))
|
}));
|
||||||
}
|
};
|
||||||
|
|
||||||
// Submit handler for server configuration
|
// Submit handler for server configuration
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault();
|
||||||
setError(null)
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const env: Record<string, string> = {}
|
const env: Record<string, string> = {};
|
||||||
envVars.forEach(({ key, value }) => {
|
envVars.forEach(({ key, value }) => {
|
||||||
if (key.trim()) {
|
if (key.trim()) {
|
||||||
env[key.trim()] = value
|
env[key.trim()] = value;
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
const headers: Record<string, string> = {}
|
const headers: Record<string, string> = {};
|
||||||
headerVars.forEach(({ key, value }) => {
|
headerVars.forEach(({ key, value }) => {
|
||||||
if (key.trim()) {
|
if (key.trim()) {
|
||||||
headers[key.trim()] = value
|
headers[key.trim()] = value;
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
// Prepare options object, only include defined values
|
// Prepare options object, only include defined values
|
||||||
const options: any = {}
|
const options: any = {};
|
||||||
if (formData.options?.timeout && formData.options.timeout !== 60000) {
|
if (formData.options?.timeout && formData.options.timeout !== 60000) {
|
||||||
options.timeout = formData.options.timeout
|
options.timeout = formData.options.timeout;
|
||||||
}
|
}
|
||||||
if (formData.options?.resetTimeoutOnProgress) {
|
if (formData.options?.resetTimeoutOnProgress) {
|
||||||
options.resetTimeoutOnProgress = formData.options.resetTimeoutOnProgress
|
options.resetTimeoutOnProgress = formData.options.resetTimeoutOnProgress;
|
||||||
}
|
}
|
||||||
if (formData.options?.maxTotalTimeout) {
|
if (formData.options?.maxTotalTimeout) {
|
||||||
options.maxTotalTimeout = formData.options.maxTotalTimeout
|
options.maxTotalTimeout = formData.options.maxTotalTimeout;
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
@@ -191,85 +238,87 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
|||||||
type: serverType, // Always include the type
|
type: serverType, // Always include the type
|
||||||
...(serverType === 'openapi'
|
...(serverType === 'openapi'
|
||||||
? {
|
? {
|
||||||
openapi: (() => {
|
openapi: (() => {
|
||||||
const openapi: any = {
|
const openapi: any = {
|
||||||
version: formData.openapi?.version || '3.1.0'
|
version: formData.openapi?.version || '3.1.0',
|
||||||
};
|
|
||||||
|
|
||||||
// Add URL or schema based on input mode
|
|
||||||
if (formData.openapi?.inputMode === 'url') {
|
|
||||||
openapi.url = formData.openapi?.url || '';
|
|
||||||
} else if (formData.openapi?.inputMode === 'schema' && formData.openapi?.schema) {
|
|
||||||
try {
|
|
||||||
openapi.schema = JSON.parse(formData.openapi.schema);
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error('Invalid JSON schema format');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add security configuration if provided
|
|
||||||
if (formData.openapi?.securityType && formData.openapi.securityType !== 'none') {
|
|
||||||
openapi.security = {
|
|
||||||
type: formData.openapi.securityType,
|
|
||||||
...(formData.openapi.securityType === 'apiKey' && {
|
|
||||||
apiKey: {
|
|
||||||
name: formData.openapi.apiKeyName || '',
|
|
||||||
in: formData.openapi.apiKeyIn || 'header',
|
|
||||||
value: formData.openapi.apiKeyValue || ''
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
...(formData.openapi.securityType === 'http' && {
|
|
||||||
http: {
|
|
||||||
scheme: formData.openapi.httpScheme || 'bearer',
|
|
||||||
credentials: formData.openapi.httpCredentials || ''
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
...(formData.openapi.securityType === 'oauth2' && {
|
|
||||||
oauth2: {
|
|
||||||
token: formData.openapi.oauth2Token || ''
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
...(formData.openapi.securityType === 'openIdConnect' && {
|
|
||||||
openIdConnect: {
|
|
||||||
url: formData.openapi.openIdConnectUrl || '',
|
|
||||||
token: formData.openapi.openIdConnectToken || ''
|
|
||||||
}
|
|
||||||
})
|
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
// Add passthrough headers if provided
|
// Add URL or schema based on input mode
|
||||||
if (formData.openapi?.passthroughHeaders && formData.openapi.passthroughHeaders.trim()) {
|
if (formData.openapi?.inputMode === 'url') {
|
||||||
openapi.passthroughHeaders = formData.openapi.passthroughHeaders
|
openapi.url = formData.openapi?.url || '';
|
||||||
.split(',')
|
} else if (formData.openapi?.inputMode === 'schema' && formData.openapi?.schema) {
|
||||||
.map(header => header.trim())
|
try {
|
||||||
.filter(header => header.length > 0);
|
openapi.schema = JSON.parse(formData.openapi.schema);
|
||||||
}
|
} catch (e) {
|
||||||
|
throw new Error('Invalid JSON schema format');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return openapi;
|
// Add security configuration if provided
|
||||||
})(),
|
if (formData.openapi?.securityType && formData.openapi.securityType !== 'none') {
|
||||||
...(Object.keys(headers).length > 0 ? { headers } : {})
|
openapi.security = {
|
||||||
}
|
type: formData.openapi.securityType,
|
||||||
|
...(formData.openapi.securityType === 'apiKey' && {
|
||||||
|
apiKey: {
|
||||||
|
name: formData.openapi.apiKeyName || '',
|
||||||
|
in: formData.openapi.apiKeyIn || 'header',
|
||||||
|
value: formData.openapi.apiKeyValue || '',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
...(formData.openapi.securityType === 'http' && {
|
||||||
|
http: {
|
||||||
|
scheme: formData.openapi.httpScheme || 'bearer',
|
||||||
|
credentials: formData.openapi.httpCredentials || '',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
...(formData.openapi.securityType === 'oauth2' && {
|
||||||
|
oauth2: {
|
||||||
|
token: formData.openapi.oauth2Token || '',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
...(formData.openapi.securityType === 'openIdConnect' && {
|
||||||
|
openIdConnect: {
|
||||||
|
url: formData.openapi.openIdConnectUrl || '',
|
||||||
|
token: formData.openapi.openIdConnectToken || '',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add passthrough headers if provided
|
||||||
|
if (
|
||||||
|
formData.openapi?.passthroughHeaders &&
|
||||||
|
formData.openapi.passthroughHeaders.trim()
|
||||||
|
) {
|
||||||
|
openapi.passthroughHeaders = formData.openapi.passthroughHeaders
|
||||||
|
.split(',')
|
||||||
|
.map((header) => header.trim())
|
||||||
|
.filter((header) => header.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return openapi;
|
||||||
|
})(),
|
||||||
|
...(Object.keys(headers).length > 0 ? { headers } : {}),
|
||||||
|
}
|
||||||
: serverType === 'sse' || serverType === 'streamable-http'
|
: serverType === 'sse' || serverType === 'streamable-http'
|
||||||
? {
|
? {
|
||||||
url: formData.url,
|
url: formData.url,
|
||||||
...(Object.keys(headers).length > 0 ? { headers } : {})
|
...(Object.keys(headers).length > 0 ? { headers } : {}),
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
command: formData.command,
|
command: formData.command,
|
||||||
args: formData.args,
|
args: formData.args,
|
||||||
env: Object.keys(env).length > 0 ? env : undefined,
|
env: Object.keys(env).length > 0 ? env : undefined,
|
||||||
}
|
}),
|
||||||
),
|
...(Object.keys(options).length > 0 ? { options } : {}),
|
||||||
...(Object.keys(options).length > 0 ? { options } : {})
|
},
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
onSubmit(payload)
|
onSubmit(payload);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(`Error: ${err instanceof Error ? err.message : String(err)}`)
|
setError(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white shadow rounded-lg p-6 w-full max-w-xl max-h-screen overflow-y-auto">
|
<div className="bg-white shadow rounded-lg p-6 w-full max-w-xl max-h-screen overflow-y-auto">
|
||||||
@@ -281,9 +330,7 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(error || formError) && (
|
{(error || formError) && (
|
||||||
<div className="bg-red-50 text-red-700 p-3 rounded mb-4">
|
<div className="bg-red-50 text-red-700 p-3 rounded mb-4">{formError || error}</div>
|
||||||
{formError || error}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
@@ -373,10 +420,12 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
|||||||
name="inputMode"
|
name="inputMode"
|
||||||
value="url"
|
value="url"
|
||||||
checked={formData.openapi?.inputMode === 'url'}
|
checked={formData.openapi?.inputMode === 'url'}
|
||||||
onChange={() => setFormData(prev => ({
|
onChange={() =>
|
||||||
...prev,
|
setFormData((prev) => ({
|
||||||
openapi: { ...prev.openapi!, inputMode: 'url' }
|
...prev,
|
||||||
}))}
|
openapi: { ...prev.openapi!, inputMode: 'url' },
|
||||||
|
}))
|
||||||
|
}
|
||||||
className="mr-1"
|
className="mr-1"
|
||||||
/>
|
/>
|
||||||
<label htmlFor="input-mode-url">{t('server.openapi.inputModeUrl')}</label>
|
<label htmlFor="input-mode-url">{t('server.openapi.inputModeUrl')}</label>
|
||||||
@@ -388,10 +437,12 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
|||||||
name="inputMode"
|
name="inputMode"
|
||||||
value="schema"
|
value="schema"
|
||||||
checked={formData.openapi?.inputMode === 'schema'}
|
checked={formData.openapi?.inputMode === 'schema'}
|
||||||
onChange={() => setFormData(prev => ({
|
onChange={() =>
|
||||||
...prev,
|
setFormData((prev) => ({
|
||||||
openapi: { ...prev.openapi!, inputMode: 'schema' }
|
...prev,
|
||||||
}))}
|
openapi: { ...prev.openapi!, inputMode: 'schema' },
|
||||||
|
}))
|
||||||
|
}
|
||||||
className="mr-1"
|
className="mr-1"
|
||||||
/>
|
/>
|
||||||
<label htmlFor="input-mode-schema">{t('server.openapi.inputModeSchema')}</label>
|
<label htmlFor="input-mode-schema">{t('server.openapi.inputModeSchema')}</label>
|
||||||
@@ -410,10 +461,12 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
|||||||
name="openapi-url"
|
name="openapi-url"
|
||||||
id="openapi-url"
|
id="openapi-url"
|
||||||
value={formData.openapi?.url || ''}
|
value={formData.openapi?.url || ''}
|
||||||
onChange={(e) => setFormData(prev => ({
|
onChange={(e) =>
|
||||||
...prev,
|
setFormData((prev) => ({
|
||||||
openapi: { ...prev.openapi!, url: e.target.value }
|
...prev,
|
||||||
}))}
|
openapi: { ...prev.openapi!, url: 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"
|
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="e.g.: https://api.example.com/openapi.json"
|
placeholder="e.g.: https://api.example.com/openapi.json"
|
||||||
required={serverType === 'openapi' && formData.openapi?.inputMode === 'url'}
|
required={serverType === 'openapi' && formData.openapi?.inputMode === 'url'}
|
||||||
@@ -424,7 +477,10 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
|||||||
{/* Schema Input */}
|
{/* Schema Input */}
|
||||||
{formData.openapi?.inputMode === 'schema' && (
|
{formData.openapi?.inputMode === 'schema' && (
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="openapi-schema">
|
<label
|
||||||
|
className="block text-gray-700 text-sm font-bold mb-2"
|
||||||
|
htmlFor="openapi-schema"
|
||||||
|
>
|
||||||
{t('server.openapi.schema')}
|
{t('server.openapi.schema')}
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
@@ -432,10 +488,12 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
|||||||
id="openapi-schema"
|
id="openapi-schema"
|
||||||
rows={10}
|
rows={10}
|
||||||
value={formData.openapi?.schema || ''}
|
value={formData.openapi?.schema || ''}
|
||||||
onChange={(e) => setFormData(prev => ({
|
onChange={(e) =>
|
||||||
...prev,
|
setFormData((prev) => ({
|
||||||
openapi: { ...prev.openapi!, schema: e.target.value }
|
...prev,
|
||||||
}))}
|
openapi: { ...prev.openapi!, schema: 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 font-mono text-sm"
|
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline font-mono text-sm"
|
||||||
placeholder={`{
|
placeholder={`{
|
||||||
"openapi": "3.1.0",
|
"openapi": "3.1.0",
|
||||||
@@ -465,14 +523,16 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
|||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={formData.openapi?.securityType || 'none'}
|
value={formData.openapi?.securityType || 'none'}
|
||||||
onChange={(e) => setFormData(prev => ({
|
onChange={(e) =>
|
||||||
...prev,
|
setFormData((prev) => ({
|
||||||
openapi: {
|
...prev,
|
||||||
...prev.openapi,
|
openapi: {
|
||||||
securityType: e.target.value as any,
|
...prev.openapi,
|
||||||
url: prev.openapi?.url || ''
|
securityType: e.target.value as any,
|
||||||
}
|
url: prev.openapi?.url || '',
|
||||||
}))}
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
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"
|
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"
|
||||||
>
|
>
|
||||||
<option value="none">{t('server.openapi.securityNone')}</option>
|
<option value="none">{t('server.openapi.securityNone')}</option>
|
||||||
@@ -486,29 +546,47 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
|||||||
{/* API Key Configuration */}
|
{/* API Key Configuration */}
|
||||||
{formData.openapi?.securityType === 'apiKey' && (
|
{formData.openapi?.securityType === 'apiKey' && (
|
||||||
<div className="mb-4 p-4 border border-gray-200 rounded bg-gray-50">
|
<div className="mb-4 p-4 border border-gray-200 rounded bg-gray-50">
|
||||||
<h4 className="text-sm font-medium text-gray-700 mb-3">{t('server.openapi.apiKeyConfig')}</h4>
|
<h4 className="text-sm font-medium text-gray-700 mb-3">
|
||||||
|
{t('server.openapi.apiKeyConfig')}
|
||||||
|
</h4>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs text-gray-600 mb-1">{t('server.openapi.apiKeyName')}</label>
|
<label className="block text-xs text-gray-600 mb-1">
|
||||||
|
{t('server.openapi.apiKeyName')}
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.openapi?.apiKeyName || ''}
|
value={formData.openapi?.apiKeyName || ''}
|
||||||
onChange={(e) => setFormData(prev => ({
|
onChange={(e) =>
|
||||||
...prev,
|
setFormData((prev) => ({
|
||||||
openapi: { ...prev.openapi, apiKeyName: e.target.value, url: prev.openapi?.url || '' }
|
...prev,
|
||||||
}))}
|
openapi: {
|
||||||
|
...prev.openapi,
|
||||||
|
apiKeyName: e.target.value,
|
||||||
|
url: prev.openapi?.url || '',
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
className="w-full border rounded px-2 py-1 text-sm form-input focus:outline-none"
|
className="w-full border rounded px-2 py-1 text-sm form-input focus:outline-none"
|
||||||
placeholder="Authorization"
|
placeholder="Authorization"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs text-gray-600 mb-1">{t('server.openapi.apiKeyIn')}</label>
|
<label className="block text-xs text-gray-600 mb-1">
|
||||||
|
{t('server.openapi.apiKeyIn')}
|
||||||
|
</label>
|
||||||
<select
|
<select
|
||||||
value={formData.openapi?.apiKeyIn || 'header'}
|
value={formData.openapi?.apiKeyIn || 'header'}
|
||||||
onChange={(e) => setFormData(prev => ({
|
onChange={(e) =>
|
||||||
...prev,
|
setFormData((prev) => ({
|
||||||
openapi: { ...prev.openapi, apiKeyIn: e.target.value as any, url: prev.openapi?.url || '' }
|
...prev,
|
||||||
}))}
|
openapi: {
|
||||||
|
...prev.openapi,
|
||||||
|
apiKeyIn: e.target.value as any,
|
||||||
|
url: prev.openapi?.url || '',
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
className="w-full border rounded px-2 py-1 text-sm focus:outline-none form-input"
|
className="w-full border rounded px-2 py-1 text-sm focus:outline-none form-input"
|
||||||
>
|
>
|
||||||
<option value="header">{t('server.openapi.apiKeyInHeader')}</option>
|
<option value="header">{t('server.openapi.apiKeyInHeader')}</option>
|
||||||
@@ -517,14 +595,22 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs text-gray-600 mb-1">{t('server.openapi.apiKeyValue')}</label>
|
<label className="block text-xs text-gray-600 mb-1">
|
||||||
|
{t('server.openapi.apiKeyValue')}
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
value={formData.openapi?.apiKeyValue || ''}
|
value={formData.openapi?.apiKeyValue || ''}
|
||||||
onChange={(e) => setFormData(prev => ({
|
onChange={(e) =>
|
||||||
...prev,
|
setFormData((prev) => ({
|
||||||
openapi: { ...prev.openapi, apiKeyValue: e.target.value, url: prev.openapi?.url || '' }
|
...prev,
|
||||||
}))}
|
openapi: {
|
||||||
|
...prev.openapi,
|
||||||
|
apiKeyValue: e.target.value,
|
||||||
|
url: prev.openapi?.url || '',
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
className="w-full border rounded px-2 py-1 text-sm focus:outline-none form-input"
|
className="w-full border rounded px-2 py-1 text-sm focus:outline-none form-input"
|
||||||
placeholder="your-api-key"
|
placeholder="your-api-key"
|
||||||
/>
|
/>
|
||||||
@@ -536,16 +622,26 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
|||||||
{/* HTTP Authentication Configuration */}
|
{/* HTTP Authentication Configuration */}
|
||||||
{formData.openapi?.securityType === 'http' && (
|
{formData.openapi?.securityType === 'http' && (
|
||||||
<div className="mb-4 p-4 border border-gray-200 rounded bg-gray-50">
|
<div className="mb-4 p-4 border border-gray-200 rounded bg-gray-50">
|
||||||
<h4 className="text-sm font-medium text-gray-700 mb-3">{t('server.openapi.httpAuthConfig')}</h4>
|
<h4 className="text-sm font-medium text-gray-700 mb-3">
|
||||||
|
{t('server.openapi.httpAuthConfig')}
|
||||||
|
</h4>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs text-gray-600 mb-1">{t('server.openapi.httpScheme')}</label>
|
<label className="block text-xs text-gray-600 mb-1">
|
||||||
|
{t('server.openapi.httpScheme')}
|
||||||
|
</label>
|
||||||
<select
|
<select
|
||||||
value={formData.openapi?.httpScheme || 'bearer'}
|
value={formData.openapi?.httpScheme || 'bearer'}
|
||||||
onChange={(e) => setFormData(prev => ({
|
onChange={(e) =>
|
||||||
...prev,
|
setFormData((prev) => ({
|
||||||
openapi: { ...prev.openapi, httpScheme: e.target.value as any, url: prev.openapi?.url || '' }
|
...prev,
|
||||||
}))}
|
openapi: {
|
||||||
|
...prev.openapi,
|
||||||
|
httpScheme: e.target.value as any,
|
||||||
|
url: prev.openapi?.url || '',
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
className="w-full border rounded px-2 py-1 text-sm focus:outline-none form-input"
|
className="w-full border rounded px-2 py-1 text-sm focus:outline-none form-input"
|
||||||
>
|
>
|
||||||
<option value="basic">{t('server.openapi.httpSchemeBasic')}</option>
|
<option value="basic">{t('server.openapi.httpSchemeBasic')}</option>
|
||||||
@@ -554,16 +650,28 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs text-gray-600 mb-1">{t('server.openapi.httpCredentials')}</label>
|
<label className="block text-xs text-gray-600 mb-1">
|
||||||
|
{t('server.openapi.httpCredentials')}
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
value={formData.openapi?.httpCredentials || ''}
|
value={formData.openapi?.httpCredentials || ''}
|
||||||
onChange={(e) => setFormData(prev => ({
|
onChange={(e) =>
|
||||||
...prev,
|
setFormData((prev) => ({
|
||||||
openapi: { ...prev.openapi, httpCredentials: e.target.value, url: prev.openapi?.url || '' }
|
...prev,
|
||||||
}))}
|
openapi: {
|
||||||
|
...prev.openapi,
|
||||||
|
httpCredentials: e.target.value,
|
||||||
|
url: prev.openapi?.url || '',
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
className="w-full border rounded px-2 py-1 text-sm focus:outline-none form-input"
|
className="w-full border rounded px-2 py-1 text-sm focus:outline-none form-input"
|
||||||
placeholder={formData.openapi?.httpScheme === 'basic' ? 'base64-encoded-credentials' : 'bearer-token'}
|
placeholder={
|
||||||
|
formData.openapi?.httpScheme === 'basic'
|
||||||
|
? 'base64-encoded-credentials'
|
||||||
|
: 'bearer-token'
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -573,17 +681,27 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
|||||||
{/* OAuth2 Configuration */}
|
{/* OAuth2 Configuration */}
|
||||||
{formData.openapi?.securityType === 'oauth2' && (
|
{formData.openapi?.securityType === 'oauth2' && (
|
||||||
<div className="mb-4 p-4 border border-gray-200 rounded bg-gray-50">
|
<div className="mb-4 p-4 border border-gray-200 rounded bg-gray-50">
|
||||||
<h4 className="text-sm font-medium text-gray-700 mb-3">{t('server.openapi.oauth2Config')}</h4>
|
<h4 className="text-sm font-medium text-gray-700 mb-3">
|
||||||
|
{t('server.openapi.oauth2Config')}
|
||||||
|
</h4>
|
||||||
<div className="grid grid-cols-1 gap-3">
|
<div className="grid grid-cols-1 gap-3">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs text-gray-600 mb-1">{t('server.openapi.oauth2Token')}</label>
|
<label className="block text-xs text-gray-600 mb-1">
|
||||||
|
{t('server.openapi.oauth2Token')}
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
value={formData.openapi?.oauth2Token || ''}
|
value={formData.openapi?.oauth2Token || ''}
|
||||||
onChange={(e) => setFormData(prev => ({
|
onChange={(e) =>
|
||||||
...prev,
|
setFormData((prev) => ({
|
||||||
openapi: { ...prev.openapi, oauth2Token: e.target.value, url: prev.openapi?.url || '' }
|
...prev,
|
||||||
}))}
|
openapi: {
|
||||||
|
...prev.openapi,
|
||||||
|
oauth2Token: e.target.value,
|
||||||
|
url: prev.openapi?.url || '',
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
className="w-full border rounded px-2 py-1 text-sm focus:outline-none form-input"
|
className="w-full border rounded px-2 py-1 text-sm focus:outline-none form-input"
|
||||||
placeholder="access-token"
|
placeholder="access-token"
|
||||||
/>
|
/>
|
||||||
@@ -595,30 +713,48 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
|||||||
{/* OpenID Connect Configuration */}
|
{/* OpenID Connect Configuration */}
|
||||||
{formData.openapi?.securityType === 'openIdConnect' && (
|
{formData.openapi?.securityType === 'openIdConnect' && (
|
||||||
<div className="mb-4 p-4 border border-gray-200 rounded bg-gray-50">
|
<div className="mb-4 p-4 border border-gray-200 rounded bg-gray-50">
|
||||||
<h4 className="text-sm font-medium text-gray-700 mb-3">{t('server.openapi.openIdConnectConfig')}</h4>
|
<h4 className="text-sm font-medium text-gray-700 mb-3">
|
||||||
|
{t('server.openapi.openIdConnectConfig')}
|
||||||
|
</h4>
|
||||||
<div className="grid grid-cols-1 gap-3">
|
<div className="grid grid-cols-1 gap-3">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs text-gray-600 mb-1">{t('server.openapi.openIdConnectUrl')}</label>
|
<label className="block text-xs text-gray-600 mb-1">
|
||||||
|
{t('server.openapi.openIdConnectUrl')}
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
type="url"
|
type="url"
|
||||||
value={formData.openapi?.openIdConnectUrl || ''}
|
value={formData.openapi?.openIdConnectUrl || ''}
|
||||||
onChange={(e) => setFormData(prev => ({
|
onChange={(e) =>
|
||||||
...prev,
|
setFormData((prev) => ({
|
||||||
openapi: { ...prev.openapi, openIdConnectUrl: e.target.value, url: prev.openapi?.url || '' }
|
...prev,
|
||||||
}))}
|
openapi: {
|
||||||
|
...prev.openapi,
|
||||||
|
openIdConnectUrl: e.target.value,
|
||||||
|
url: prev.openapi?.url || '',
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
className="w-full border rounded px-2 py-1 text-sm focus:outline-none form-input"
|
className="w-full border rounded px-2 py-1 text-sm focus:outline-none form-input"
|
||||||
placeholder="https://example.com/.well-known/openid_configuration"
|
placeholder="https://example.com/.well-known/openid_configuration"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs text-gray-600 mb-1">{t('server.openapi.openIdConnectToken')}</label>
|
<label className="block text-xs text-gray-600 mb-1">
|
||||||
|
{t('server.openapi.openIdConnectToken')}
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
value={formData.openapi?.openIdConnectToken || ''}
|
value={formData.openapi?.openIdConnectToken || ''}
|
||||||
onChange={(e) => setFormData(prev => ({
|
onChange={(e) =>
|
||||||
...prev,
|
setFormData((prev) => ({
|
||||||
openapi: { ...prev.openapi, openIdConnectToken: e.target.value, url: prev.openapi?.url || '' }
|
...prev,
|
||||||
}))}
|
openapi: {
|
||||||
|
...prev.openapi,
|
||||||
|
openIdConnectToken: e.target.value,
|
||||||
|
url: prev.openapi?.url || '',
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
className="w-full border rounded px-2 py-1 text-sm focus:outline-none form-input"
|
className="w-full border rounded px-2 py-1 text-sm focus:outline-none form-input"
|
||||||
placeholder="id-token"
|
placeholder="id-token"
|
||||||
/>
|
/>
|
||||||
@@ -635,14 +771,22 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.openapi?.passthroughHeaders || ''}
|
value={formData.openapi?.passthroughHeaders || ''}
|
||||||
onChange={(e) => setFormData(prev => ({
|
onChange={(e) =>
|
||||||
...prev,
|
setFormData((prev) => ({
|
||||||
openapi: { ...prev.openapi, passthroughHeaders: e.target.value, url: prev.openapi?.url || '' }
|
...prev,
|
||||||
}))}
|
openapi: {
|
||||||
|
...prev.openapi,
|
||||||
|
passthroughHeaders: e.target.value,
|
||||||
|
url: prev.openapi?.url || '',
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
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"
|
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="Authorization, X-API-Key, X-Custom-Header"
|
placeholder="Authorization, X-API-Key, X-Custom-Header"
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-gray-500 mt-1">{t('server.openapi.passthroughHeadersHelp')}</p>
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
|
{t('server.openapi.passthroughHeadersHelp')}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
@@ -701,7 +845,11 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
|||||||
value={formData.url}
|
value={formData.url}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
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"
|
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={serverType === 'streamable-http' ? "e.g.: http://localhost:3000/mcp" : "e.g.: http://localhost:3000/sse"}
|
placeholder={
|
||||||
|
serverType === 'streamable-http'
|
||||||
|
? 'e.g.: http://localhost:3000/mcp'
|
||||||
|
: 'e.g.: http://localhost:3000/sse'
|
||||||
|
}
|
||||||
required={serverType === 'sse' || serverType === 'streamable-http'}
|
required={serverType === 'sse' || serverType === 'streamable-http'}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -837,23 +985,26 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
|||||||
<label className="text-gray-700 text-sm font-bold">
|
<label className="text-gray-700 text-sm font-bold">
|
||||||
{t('server.requestOptions')}
|
{t('server.requestOptions')}
|
||||||
</label>
|
</label>
|
||||||
<span className="text-gray-500 text-sm">
|
<span className="text-gray-500 text-sm">{isRequestOptionsExpanded ? '▼' : '▶'}</span>
|
||||||
{isRequestOptionsExpanded ? '▼' : '▶'}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isRequestOptionsExpanded && (
|
{isRequestOptionsExpanded && (
|
||||||
<div className="border border-gray-200 rounded-b p-4 bg-gray-50 border-t-0">
|
<div className="border border-gray-200 rounded-b p-4 bg-gray-50 border-t-0">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-gray-600 text-sm font-medium mb-1" htmlFor="timeout">
|
<label
|
||||||
|
className="block text-gray-600 text-sm font-medium mb-1"
|
||||||
|
htmlFor="timeout"
|
||||||
|
>
|
||||||
{t('server.timeout')}
|
{t('server.timeout')}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
id="timeout"
|
id="timeout"
|
||||||
value={formData.options?.timeout || 60000}
|
value={formData.options?.timeout || 60000}
|
||||||
onChange={(e) => handleOptionsChange('timeout', parseInt(e.target.value) || 60000)}
|
onChange={(e) =>
|
||||||
|
handleOptionsChange('timeout', parseInt(e.target.value) || 60000)
|
||||||
|
}
|
||||||
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"
|
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="30000"
|
placeholder="30000"
|
||||||
min="1000"
|
min="1000"
|
||||||
@@ -863,19 +1014,29 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-gray-600 text-sm font-medium mb-1" htmlFor="maxTotalTimeout">
|
<label
|
||||||
|
className="block text-gray-600 text-sm font-medium mb-1"
|
||||||
|
htmlFor="maxTotalTimeout"
|
||||||
|
>
|
||||||
{t('server.maxTotalTimeout')}
|
{t('server.maxTotalTimeout')}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
id="maxTotalTimeout"
|
id="maxTotalTimeout"
|
||||||
value={formData.options?.maxTotalTimeout || ''}
|
value={formData.options?.maxTotalTimeout || ''}
|
||||||
onChange={(e) => handleOptionsChange('maxTotalTimeout', e.target.value ? parseInt(e.target.value) : undefined)}
|
onChange={(e) =>
|
||||||
|
handleOptionsChange(
|
||||||
|
'maxTotalTimeout',
|
||||||
|
e.target.value ? parseInt(e.target.value) : undefined,
|
||||||
|
)
|
||||||
|
}
|
||||||
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"
|
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="Optional"
|
placeholder="Optional"
|
||||||
min="1000"
|
min="1000"
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-gray-500 mt-1">{t('server.maxTotalTimeoutDescription')}</p>
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
|
{t('server.maxTotalTimeoutDescription')}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -884,10 +1045,14 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
|||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={formData.options?.resetTimeoutOnProgress || false}
|
checked={formData.options?.resetTimeoutOnProgress || false}
|
||||||
onChange={(e) => handleOptionsChange('resetTimeoutOnProgress', e.target.checked)}
|
onChange={(e) =>
|
||||||
|
handleOptionsChange('resetTimeoutOnProgress', e.target.checked)
|
||||||
|
}
|
||||||
className="mr-2"
|
className="mr-2"
|
||||||
/>
|
/>
|
||||||
<span className="text-gray-600 text-sm">{t('server.resetTimeoutOnProgress')}</span>
|
<span className="text-gray-600 text-sm">
|
||||||
|
{t('server.resetTimeoutOnProgress')}
|
||||||
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<p className="text-xs text-gray-500 mt-1 ml-6">
|
<p className="text-xs text-gray-500 mt-1 ml-6">
|
||||||
{t('server.resetTimeoutOnProgressDescription')}
|
{t('server.resetTimeoutOnProgressDescription')}
|
||||||
@@ -915,7 +1080,7 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default ServerForm
|
export default ServerForm;
|
||||||
|
|||||||
78
frontend/src/components/ui/CursorPagination.tsx
Normal file
78
frontend/src/components/ui/CursorPagination.tsx
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface CursorPaginationProps {
|
||||||
|
currentPage: number;
|
||||||
|
hasNextPage: boolean;
|
||||||
|
hasPreviousPage: boolean;
|
||||||
|
onNextPage: () => void;
|
||||||
|
onPreviousPage: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CursorPagination: React.FC<CursorPaginationProps> = ({
|
||||||
|
currentPage,
|
||||||
|
hasNextPage,
|
||||||
|
hasPreviousPage,
|
||||||
|
onNextPage,
|
||||||
|
onPreviousPage,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center space-x-2 my-6">
|
||||||
|
{/* Previous button */}
|
||||||
|
<button
|
||||||
|
onClick={onPreviousPage}
|
||||||
|
disabled={!hasPreviousPage}
|
||||||
|
className={`px-4 py-2 rounded transition-all duration-200 ${
|
||||||
|
hasPreviousPage
|
||||||
|
? 'bg-gray-200 hover:bg-gray-300 text-gray-700 btn-secondary'
|
||||||
|
: 'bg-gray-100 text-gray-400 cursor-not-allowed'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className="h-5 w-5 inline-block mr-1"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Prev
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Current page indicator */}
|
||||||
|
<span className="px-4 py-2 bg-blue-500 text-white rounded btn-primary">
|
||||||
|
Page {currentPage}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{/* Next button */}
|
||||||
|
<button
|
||||||
|
onClick={onNextPage}
|
||||||
|
disabled={!hasNextPage}
|
||||||
|
className={`px-4 py-2 rounded transition-all duration-200 ${
|
||||||
|
hasNextPage
|
||||||
|
? 'bg-gray-200 hover:bg-gray-300 text-gray-700 btn-secondary'
|
||||||
|
: 'bg-gray-100 text-gray-400 cursor-not-allowed'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className="h-5 w-5 inline-block ml-1"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CursorPagination;
|
||||||
@@ -63,55 +63,58 @@ export const ServerProvider: React.FC<{ children: React.ReactNode }> = ({ childr
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Start normal polling
|
// Start normal polling
|
||||||
const startNormalPolling = useCallback((options?: { immediate?: boolean }) => {
|
const startNormalPolling = useCallback(
|
||||||
const immediate = options?.immediate ?? true;
|
(options?: { immediate?: boolean }) => {
|
||||||
// Ensure no other timers are running
|
const immediate = options?.immediate ?? true;
|
||||||
clearTimer();
|
// Ensure no other timers are running
|
||||||
|
clearTimer();
|
||||||
|
|
||||||
const fetchServers = async () => {
|
const fetchServers = async () => {
|
||||||
try {
|
try {
|
||||||
console.log('[ServerContext] Fetching servers from API...');
|
console.log('[ServerContext] Fetching servers from API...');
|
||||||
const data = await apiGet('/servers');
|
const data = await apiGet('/servers');
|
||||||
|
|
||||||
// Update last fetch time
|
|
||||||
lastFetchTimeRef.current = Date.now();
|
|
||||||
|
|
||||||
if (data && data.success && Array.isArray(data.data)) {
|
// Update last fetch time
|
||||||
setServers(data.data);
|
lastFetchTimeRef.current = Date.now();
|
||||||
} else if (data && Array.isArray(data)) {
|
|
||||||
setServers(data);
|
if (data && data.success && Array.isArray(data.data)) {
|
||||||
} else {
|
setServers(data.data);
|
||||||
console.error('Invalid server data format:', data);
|
} else if (data && Array.isArray(data)) {
|
||||||
setServers([]);
|
setServers(data);
|
||||||
|
} else {
|
||||||
|
console.error('Invalid server data format:', data);
|
||||||
|
setServers([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset error state
|
||||||
|
setError(null);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching servers during normal polling:', err);
|
||||||
|
|
||||||
|
// Use friendly error message
|
||||||
|
if (!navigator.onLine) {
|
||||||
|
setError(t('errors.network'));
|
||||||
|
} else if (
|
||||||
|
err instanceof TypeError &&
|
||||||
|
(err.message.includes('NetworkError') || err.message.includes('Failed to fetch'))
|
||||||
|
) {
|
||||||
|
setError(t('errors.serverConnection'));
|
||||||
|
} else {
|
||||||
|
setError(t('errors.serverFetch'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Reset error state
|
// Execute immediately unless explicitly skipped
|
||||||
setError(null);
|
if (immediate) {
|
||||||
} catch (err) {
|
fetchServers();
|
||||||
console.error('Error fetching servers during normal polling:', err);
|
|
||||||
|
|
||||||
// Use friendly error message
|
|
||||||
if (!navigator.onLine) {
|
|
||||||
setError(t('errors.network'));
|
|
||||||
} else if (
|
|
||||||
err instanceof TypeError &&
|
|
||||||
(err.message.includes('NetworkError') || err.message.includes('Failed to fetch'))
|
|
||||||
) {
|
|
||||||
setError(t('errors.serverConnection'));
|
|
||||||
} else {
|
|
||||||
setError(t('errors.serverFetch'));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
// Execute immediately unless explicitly skipped
|
// Set up regular polling
|
||||||
if (immediate) {
|
intervalRef.current = setInterval(fetchServers, CONFIG.normal.pollingInterval);
|
||||||
fetchServers();
|
},
|
||||||
}
|
[t],
|
||||||
|
);
|
||||||
// Set up regular polling
|
|
||||||
intervalRef.current = setInterval(fetchServers, CONFIG.normal.pollingInterval);
|
|
||||||
}, [t]);
|
|
||||||
|
|
||||||
// Watch for authentication status changes
|
// Watch for authentication status changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -147,7 +150,7 @@ export const ServerProvider: React.FC<{ children: React.ReactNode }> = ({ childr
|
|||||||
try {
|
try {
|
||||||
console.log('[ServerContext] Initial fetch - attempt', attemptsRef.current + 1);
|
console.log('[ServerContext] Initial fetch - attempt', attemptsRef.current + 1);
|
||||||
const data = await apiGet('/servers');
|
const data = await apiGet('/servers');
|
||||||
|
|
||||||
// Update last fetch time
|
// Update last fetch time
|
||||||
lastFetchTimeRef.current = Date.now();
|
lastFetchTimeRef.current = Date.now();
|
||||||
|
|
||||||
@@ -245,16 +248,30 @@ export const ServerProvider: React.FC<{ children: React.ReactNode }> = ({ childr
|
|||||||
const refreshIfNeeded = useCallback(() => {
|
const refreshIfNeeded = useCallback(() => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const timeSinceLastFetch = now - lastFetchTimeRef.current;
|
const timeSinceLastFetch = now - lastFetchTimeRef.current;
|
||||||
|
|
||||||
// Log who is calling this
|
// Log who is calling this
|
||||||
console.log('[ServerContext] refreshIfNeeded called, time since last fetch:', timeSinceLastFetch, 'ms');
|
console.log(
|
||||||
|
'[ServerContext] refreshIfNeeded called, time since last fetch:',
|
||||||
|
timeSinceLastFetch,
|
||||||
|
'ms',
|
||||||
|
);
|
||||||
|
|
||||||
// Only refresh if enough time has passed since last fetch
|
// Only refresh if enough time has passed since last fetch
|
||||||
if (timeSinceLastFetch >= MIN_REFRESH_INTERVAL) {
|
if (timeSinceLastFetch >= MIN_REFRESH_INTERVAL) {
|
||||||
console.log('[ServerContext] Triggering refresh (exceeded MIN_REFRESH_INTERVAL:', MIN_REFRESH_INTERVAL, 'ms)');
|
console.log(
|
||||||
|
'[ServerContext] Triggering refresh (exceeded MIN_REFRESH_INTERVAL:',
|
||||||
|
MIN_REFRESH_INTERVAL,
|
||||||
|
'ms)',
|
||||||
|
);
|
||||||
triggerRefresh();
|
triggerRefresh();
|
||||||
} else {
|
} else {
|
||||||
console.log('[ServerContext] Skipping refresh (MIN_REFRESH_INTERVAL:', MIN_REFRESH_INTERVAL, 'ms, time since last:', timeSinceLastFetch, 'ms)');
|
console.log(
|
||||||
|
'[ServerContext] Skipping refresh (MIN_REFRESH_INTERVAL:',
|
||||||
|
MIN_REFRESH_INTERVAL,
|
||||||
|
'ms, time since last:',
|
||||||
|
timeSinceLastFetch,
|
||||||
|
'ms)',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}, [triggerRefresh]);
|
}, [triggerRefresh]);
|
||||||
|
|
||||||
@@ -263,74 +280,85 @@ export const ServerProvider: React.FC<{ children: React.ReactNode }> = ({ childr
|
|||||||
setRefreshKey((prevKey) => prevKey + 1);
|
setRefreshKey((prevKey) => prevKey + 1);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleServerEdit = useCallback(async (server: Server) => {
|
const handleServerEdit = useCallback(
|
||||||
try {
|
async (server: Server) => {
|
||||||
// Fetch settings to get the full server config before editing
|
try {
|
||||||
const settingsData: ApiResponse<{ mcpServers: Record<string, any> }> =
|
// Fetch settings to get the full server config before editing
|
||||||
await apiGet('/settings');
|
const settingsData: ApiResponse<{ mcpServers: Record<string, any> }> =
|
||||||
|
await apiGet('/settings');
|
||||||
|
|
||||||
if (
|
if (
|
||||||
settingsData &&
|
settingsData &&
|
||||||
settingsData.success &&
|
settingsData.success &&
|
||||||
settingsData.data &&
|
settingsData.data &&
|
||||||
settingsData.data.mcpServers &&
|
settingsData.data.mcpServers &&
|
||||||
settingsData.data.mcpServers[server.name]
|
settingsData.data.mcpServers[server.name]
|
||||||
) {
|
) {
|
||||||
const serverConfig = settingsData.data.mcpServers[server.name];
|
const serverConfig = settingsData.data.mcpServers[server.name];
|
||||||
return {
|
return {
|
||||||
name: server.name,
|
name: server.name,
|
||||||
status: server.status,
|
status: server.status,
|
||||||
tools: server.tools || [],
|
tools: server.tools || [],
|
||||||
config: serverConfig,
|
config: serverConfig,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
console.error('Failed to get server config from settings:', settingsData);
|
console.error('Failed to get server config from settings:', settingsData);
|
||||||
setError(t('server.invalidConfig', { serverName: server.name }));
|
setError(t('server.invalidConfig', { serverName: server.name }));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching server settings:', err);
|
||||||
|
setError(err instanceof Error ? err.message : String(err));
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
},
|
||||||
console.error('Error fetching server settings:', err);
|
[t],
|
||||||
setError(err instanceof Error ? err.message : String(err));
|
);
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}, [t]);
|
|
||||||
|
|
||||||
const handleServerRemove = useCallback(async (serverName: string) => {
|
const handleServerRemove = useCallback(
|
||||||
try {
|
async (serverName: string) => {
|
||||||
const result = await apiDelete(`/servers/${serverName}`);
|
try {
|
||||||
|
const encodedServerName = encodeURIComponent(serverName);
|
||||||
|
const result = await apiDelete(`/servers/${encodedServerName}`);
|
||||||
|
|
||||||
if (!result || !result.success) {
|
if (!result || !result.success) {
|
||||||
setError(result?.message || t('server.deleteError', { serverName }));
|
setError(result?.message || t('server.deleteError', { serverName }));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
setRefreshKey((prevKey) => prevKey + 1);
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
setError(t('errors.general') + ': ' + (err instanceof Error ? err.message : String(err)));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
[t],
|
||||||
|
);
|
||||||
|
|
||||||
setRefreshKey((prevKey) => prevKey + 1);
|
const handleServerToggle = useCallback(
|
||||||
return true;
|
async (server: Server, enabled: boolean) => {
|
||||||
} catch (err) {
|
try {
|
||||||
setError(t('errors.general') + ': ' + (err instanceof Error ? err.message : String(err)));
|
const encodedServerName = encodeURIComponent(server.name);
|
||||||
return false;
|
const result = await apiPost(`/servers/${encodedServerName}/toggle`, { enabled });
|
||||||
}
|
|
||||||
}, [t]);
|
|
||||||
|
|
||||||
const handleServerToggle = useCallback(async (server: Server, enabled: boolean) => {
|
if (!result || !result.success) {
|
||||||
try {
|
console.error('Failed to toggle server:', result);
|
||||||
const result = await apiPost(`/servers/${server.name}/toggle`, { enabled });
|
setError(result?.message || t('server.toggleError', { serverName: server.name }));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (!result || !result.success) {
|
// Update the UI immediately to reflect the change
|
||||||
console.error('Failed to toggle server:', result);
|
setRefreshKey((prevKey) => prevKey + 1);
|
||||||
setError(result?.message || t('server.toggleError', { serverName: server.name }));
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error toggling server:', err);
|
||||||
|
setError(err instanceof Error ? err.message : String(err));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
},
|
||||||
// Update the UI immediately to reflect the change
|
[t],
|
||||||
setRefreshKey((prevKey) => prevKey + 1);
|
);
|
||||||
return true;
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error toggling server:', err);
|
|
||||||
setError(err instanceof Error ? err.message : String(err));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}, [t]);
|
|
||||||
|
|
||||||
const value: ServerContextType = {
|
const value: ServerContextType = {
|
||||||
servers,
|
servers,
|
||||||
@@ -356,4 +384,4 @@ export const useServerContext = () => {
|
|||||||
throw new Error('useServerContext must be used within a ServerProvider');
|
throw new Error('useServerContext must be used within a ServerProvider');
|
||||||
}
|
}
|
||||||
return context;
|
return context;
|
||||||
};
|
};
|
||||||
|
|||||||
283
frontend/src/hooks/useRegistryData.ts
Normal file
283
frontend/src/hooks/useRegistryData.ts
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import {
|
||||||
|
RegistryServerEntry,
|
||||||
|
RegistryServersResponse,
|
||||||
|
RegistryServerVersionResponse,
|
||||||
|
RegistryServerVersionsResponse,
|
||||||
|
} from '@/types';
|
||||||
|
import { apiGet } from '../utils/fetchInterceptor';
|
||||||
|
|
||||||
|
export const useRegistryData = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [servers, setServers] = useState<RegistryServerEntry[]>([]);
|
||||||
|
const [allServers, setAllServers] = useState<RegistryServerEntry[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [searchQuery, setSearchQuery] = useState<string>('');
|
||||||
|
|
||||||
|
// Cursor-based pagination states
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const [serversPerPage, setServersPerPage] = useState(9);
|
||||||
|
const [nextCursor, setNextCursor] = useState<string | null>(null);
|
||||||
|
const [hasNextPage, setHasNextPage] = useState(false);
|
||||||
|
const [cursorHistory, setCursorHistory] = useState<string[]>([]);
|
||||||
|
const [totalPages] = useState(1); // Legacy support, not used in cursor pagination
|
||||||
|
|
||||||
|
// Fetch registry servers with cursor-based pagination
|
||||||
|
const fetchRegistryServers = useCallback(
|
||||||
|
async (cursor?: string, search?: string) => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
// Build query parameters
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.append('limit', serversPerPage.toString());
|
||||||
|
if (cursor) {
|
||||||
|
params.append('cursor', cursor);
|
||||||
|
}
|
||||||
|
const queryToUse = search !== undefined ? search : searchQuery;
|
||||||
|
if (queryToUse.trim()) {
|
||||||
|
params.append('search', queryToUse.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await apiGet(`/registry/servers?${params.toString()}`);
|
||||||
|
|
||||||
|
if (response && response.success && response.data) {
|
||||||
|
const data: RegistryServersResponse = response.data;
|
||||||
|
if (data.servers && Array.isArray(data.servers)) {
|
||||||
|
setServers(data.servers);
|
||||||
|
// Update pagination state
|
||||||
|
const hasMore = data.metadata.count === serversPerPage && !!data.metadata.nextCursor;
|
||||||
|
setHasNextPage(hasMore);
|
||||||
|
setNextCursor(data.metadata.nextCursor || null);
|
||||||
|
|
||||||
|
// For display purposes, keep track of all loaded servers
|
||||||
|
if (!cursor) {
|
||||||
|
// First page
|
||||||
|
setAllServers(data.servers);
|
||||||
|
} else {
|
||||||
|
// Subsequent pages - append to all servers
|
||||||
|
setAllServers((prev) => [...prev, ...data.servers]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('Invalid registry servers data format:', data);
|
||||||
|
setError(t('registry.fetchError'));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setError(t('registry.fetchError'));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching registry servers:', err);
|
||||||
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
||||||
|
setError(errorMessage);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[t, serversPerPage],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Navigate to next page
|
||||||
|
const goToNextPage = useCallback(async () => {
|
||||||
|
if (!hasNextPage || !nextCursor) return;
|
||||||
|
|
||||||
|
// Save current cursor to history for back navigation
|
||||||
|
const currentCursor = cursorHistory[cursorHistory.length - 1] || '';
|
||||||
|
setCursorHistory((prev) => [...prev, currentCursor]);
|
||||||
|
|
||||||
|
setCurrentPage((prev) => prev + 1);
|
||||||
|
await fetchRegistryServers(nextCursor, searchQuery);
|
||||||
|
}, [hasNextPage, nextCursor, cursorHistory, searchQuery, fetchRegistryServers]);
|
||||||
|
|
||||||
|
// Navigate to previous page
|
||||||
|
const goToPreviousPage = useCallback(async () => {
|
||||||
|
if (currentPage <= 1) return;
|
||||||
|
|
||||||
|
// Get the previous cursor from history
|
||||||
|
const newHistory = [...cursorHistory];
|
||||||
|
newHistory.pop(); // Remove current position
|
||||||
|
const previousCursor = newHistory[newHistory.length - 1];
|
||||||
|
|
||||||
|
setCursorHistory(newHistory);
|
||||||
|
setCurrentPage((prev) => prev - 1);
|
||||||
|
|
||||||
|
// Fetch with previous cursor (undefined for first page)
|
||||||
|
await fetchRegistryServers(previousCursor || undefined, searchQuery);
|
||||||
|
}, [currentPage, cursorHistory, searchQuery, fetchRegistryServers]);
|
||||||
|
|
||||||
|
// Change page (legacy support for page number navigation)
|
||||||
|
const changePage = useCallback(
|
||||||
|
async (page: number) => {
|
||||||
|
if (page === currentPage) return;
|
||||||
|
|
||||||
|
if (page > currentPage && hasNextPage) {
|
||||||
|
await goToNextPage();
|
||||||
|
} else if (page < currentPage && currentPage > 1) {
|
||||||
|
await goToPreviousPage();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[currentPage, hasNextPage, goToNextPage, goToPreviousPage],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Change items per page
|
||||||
|
const changeServersPerPage = useCallback(
|
||||||
|
async (newServersPerPage: number) => {
|
||||||
|
setServersPerPage(newServersPerPage);
|
||||||
|
setCurrentPage(1);
|
||||||
|
setCursorHistory([]);
|
||||||
|
setAllServers([]);
|
||||||
|
await fetchRegistryServers(undefined, searchQuery);
|
||||||
|
},
|
||||||
|
[searchQuery, fetchRegistryServers],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Fetch server by name
|
||||||
|
const fetchServerByName = useCallback(
|
||||||
|
async (serverName: string) => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
// URL encode the server name
|
||||||
|
const encodedName = encodeURIComponent(serverName);
|
||||||
|
const response = await apiGet(`/registry/servers/${encodedName}/versions`);
|
||||||
|
|
||||||
|
if (response && response.success && response.data) {
|
||||||
|
const data: RegistryServerVersionsResponse = response.data;
|
||||||
|
if (data.servers && Array.isArray(data.servers) && data.servers.length > 0) {
|
||||||
|
// Return the first server entry (should be the latest or specified version)
|
||||||
|
return data.servers[0];
|
||||||
|
} else {
|
||||||
|
console.error('Invalid registry server data format:', data);
|
||||||
|
setError(t('registry.serverNotFound'));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setError(t('registry.serverNotFound'));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Error fetching registry server ${serverName}:`, err);
|
||||||
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
||||||
|
setError(errorMessage);
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[t],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Fetch all versions of a server
|
||||||
|
const fetchServerVersions = useCallback(async (serverName: string) => {
|
||||||
|
try {
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
// URL encode the server name
|
||||||
|
const encodedName = encodeURIComponent(serverName);
|
||||||
|
const response = await apiGet(`/registry/servers/${encodedName}/versions`);
|
||||||
|
|
||||||
|
if (response && response.success && response.data) {
|
||||||
|
const data: RegistryServerVersionsResponse = response.data;
|
||||||
|
if (data.servers && Array.isArray(data.servers)) {
|
||||||
|
return data.servers;
|
||||||
|
} else {
|
||||||
|
console.error('Invalid registry server versions data format:', data);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Error fetching versions for server ${serverName}:`, err);
|
||||||
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
||||||
|
setError(errorMessage);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Fetch specific version of a server
|
||||||
|
const fetchServerVersion = useCallback(async (serverName: string, version: string) => {
|
||||||
|
try {
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
// URL encode the server name and version
|
||||||
|
const encodedName = encodeURIComponent(serverName);
|
||||||
|
const encodedVersion = encodeURIComponent(version);
|
||||||
|
const response = await apiGet(`/registry/servers/${encodedName}/versions/${encodedVersion}`);
|
||||||
|
|
||||||
|
if (response && response.success && response.data) {
|
||||||
|
const data: RegistryServerVersionResponse = response.data;
|
||||||
|
if (data && data.server) {
|
||||||
|
return data;
|
||||||
|
} else {
|
||||||
|
console.error('Invalid registry server version data format:', data);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Error fetching version ${version} for server ${serverName}:`, err);
|
||||||
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
||||||
|
setError(errorMessage);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Search servers by query (client-side filtering on loaded data)
|
||||||
|
const searchServers = useCallback(
|
||||||
|
async (query: string) => {
|
||||||
|
console.log('Searching registry servers with query:', query);
|
||||||
|
setSearchQuery(query);
|
||||||
|
setCurrentPage(1);
|
||||||
|
setCursorHistory([]);
|
||||||
|
setAllServers([]);
|
||||||
|
|
||||||
|
await fetchRegistryServers(undefined, query);
|
||||||
|
},
|
||||||
|
[fetchRegistryServers],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Clear search
|
||||||
|
const clearSearch = useCallback(async () => {
|
||||||
|
setSearchQuery('');
|
||||||
|
setCurrentPage(1);
|
||||||
|
setCursorHistory([]);
|
||||||
|
setAllServers([]);
|
||||||
|
await fetchRegistryServers(undefined, '');
|
||||||
|
}, [fetchRegistryServers]);
|
||||||
|
|
||||||
|
// Initial fetch
|
||||||
|
useEffect(() => {
|
||||||
|
fetchRegistryServers(undefined, searchQuery);
|
||||||
|
// Only run on mount
|
||||||
|
// eslint-disable-next-line
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
servers,
|
||||||
|
allServers,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
setError,
|
||||||
|
searchQuery,
|
||||||
|
searchServers,
|
||||||
|
clearSearch,
|
||||||
|
fetchServerByName,
|
||||||
|
fetchServerVersions,
|
||||||
|
fetchServerVersion,
|
||||||
|
// Cursor-based pagination
|
||||||
|
currentPage,
|
||||||
|
totalPages,
|
||||||
|
hasNextPage,
|
||||||
|
hasPreviousPage: currentPage > 1,
|
||||||
|
changePage,
|
||||||
|
goToNextPage,
|
||||||
|
goToPreviousPage,
|
||||||
|
serversPerPage,
|
||||||
|
changeServersPerPage,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,17 +1,27 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||||
import { MarketServer, CloudServer, ServerConfig } from '@/types';
|
import {
|
||||||
|
MarketServer,
|
||||||
|
CloudServer,
|
||||||
|
ServerConfig,
|
||||||
|
RegistryServerEntry,
|
||||||
|
RegistryServerData,
|
||||||
|
} from '@/types';
|
||||||
import { useMarketData } from '@/hooks/useMarketData';
|
import { useMarketData } from '@/hooks/useMarketData';
|
||||||
import { useCloudData } from '@/hooks/useCloudData';
|
import { useCloudData } from '@/hooks/useCloudData';
|
||||||
|
import { useRegistryData } from '@/hooks/useRegistryData';
|
||||||
import { useToast } from '@/contexts/ToastContext';
|
import { useToast } from '@/contexts/ToastContext';
|
||||||
import { apiPost } from '@/utils/fetchInterceptor';
|
import { apiPost } from '@/utils/fetchInterceptor';
|
||||||
import MarketServerCard from '@/components/MarketServerCard';
|
import MarketServerCard from '@/components/MarketServerCard';
|
||||||
import MarketServerDetail from '@/components/MarketServerDetail';
|
import MarketServerDetail from '@/components/MarketServerDetail';
|
||||||
import CloudServerCard from '@/components/CloudServerCard';
|
import CloudServerCard from '@/components/CloudServerCard';
|
||||||
import CloudServerDetail from '@/components/CloudServerDetail';
|
import CloudServerDetail from '@/components/CloudServerDetail';
|
||||||
|
import RegistryServerCard from '@/components/RegistryServerCard';
|
||||||
|
import RegistryServerDetail from '@/components/RegistryServerDetail';
|
||||||
import MCPRouterApiKeyError from '@/components/MCPRouterApiKeyError';
|
import MCPRouterApiKeyError from '@/components/MCPRouterApiKeyError';
|
||||||
import Pagination from '@/components/ui/Pagination';
|
import Pagination from '@/components/ui/Pagination';
|
||||||
|
import CursorPagination from '@/components/ui/CursorPagination';
|
||||||
|
|
||||||
const MarketPage: React.FC = () => {
|
const MarketPage: React.FC = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -19,7 +29,7 @@ const MarketPage: React.FC = () => {
|
|||||||
const { serverName } = useParams<{ serverName?: string }>();
|
const { serverName } = useParams<{ serverName?: string }>();
|
||||||
const { showToast } = useToast();
|
const { showToast } = useToast();
|
||||||
|
|
||||||
// Get tab from URL search params, default to cloud market
|
// Get tab from URL search params
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const currentTab = searchParams.get('tab') || 'cloud';
|
const currentTab = searchParams.get('tab') || 'cloud';
|
||||||
|
|
||||||
@@ -44,10 +54,10 @@ const MarketPage: React.FC = () => {
|
|||||||
totalPages: localTotalPages,
|
totalPages: localTotalPages,
|
||||||
changePage: changeLocalPage,
|
changePage: changeLocalPage,
|
||||||
serversPerPage: localServersPerPage,
|
serversPerPage: localServersPerPage,
|
||||||
changeServersPerPage: changeLocalServersPerPage
|
changeServersPerPage: changeLocalServersPerPage,
|
||||||
} = useMarketData();
|
} = useMarketData();
|
||||||
|
|
||||||
// Cloud market data
|
// Cloud market data
|
||||||
const {
|
const {
|
||||||
servers: cloudServers,
|
servers: cloudServers,
|
||||||
allServers: allCloudServers,
|
allServers: allCloudServers,
|
||||||
@@ -61,29 +71,67 @@ const MarketPage: React.FC = () => {
|
|||||||
totalPages: cloudTotalPages,
|
totalPages: cloudTotalPages,
|
||||||
changePage: changeCloudPage,
|
changePage: changeCloudPage,
|
||||||
serversPerPage: cloudServersPerPage,
|
serversPerPage: cloudServersPerPage,
|
||||||
changeServersPerPage: changeCloudServersPerPage
|
changeServersPerPage: changeCloudServersPerPage,
|
||||||
} = useCloudData();
|
} = useCloudData();
|
||||||
|
|
||||||
|
// Registry data
|
||||||
|
const {
|
||||||
|
servers: registryServers,
|
||||||
|
allServers: allRegistryServers,
|
||||||
|
loading: registryLoading,
|
||||||
|
error: registryError,
|
||||||
|
setError: setRegistryError,
|
||||||
|
searchServers: searchRegistryServers,
|
||||||
|
clearSearch: clearRegistrySearch,
|
||||||
|
fetchServerByName: fetchRegistryServerByName,
|
||||||
|
fetchServerVersions: fetchRegistryServerVersions,
|
||||||
|
// Cursor-based pagination
|
||||||
|
currentPage: registryCurrentPage,
|
||||||
|
totalPages: registryTotalPages,
|
||||||
|
hasNextPage: registryHasNextPage,
|
||||||
|
hasPreviousPage: registryHasPreviousPage,
|
||||||
|
changePage: changeRegistryPage,
|
||||||
|
goToNextPage: goToRegistryNextPage,
|
||||||
|
goToPreviousPage: goToRegistryPreviousPage,
|
||||||
|
serversPerPage: registryServersPerPage,
|
||||||
|
changeServersPerPage: changeRegistryServersPerPage,
|
||||||
|
} = useRegistryData();
|
||||||
|
|
||||||
const [selectedServer, setSelectedServer] = useState<MarketServer | null>(null);
|
const [selectedServer, setSelectedServer] = useState<MarketServer | null>(null);
|
||||||
const [selectedCloudServer, setSelectedCloudServer] = useState<CloudServer | null>(null);
|
const [selectedCloudServer, setSelectedCloudServer] = useState<CloudServer | null>(null);
|
||||||
|
const [selectedRegistryServer, setSelectedRegistryServer] = useState<RegistryServerEntry | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const [registrySearchQuery, setRegistrySearchQuery] = useState('');
|
||||||
const [installing, setInstalling] = useState(false);
|
const [installing, setInstalling] = useState(false);
|
||||||
const [installedCloudServers, setInstalledCloudServers] = useState<Set<string>>(new Set());
|
const [installedCloudServers, setInstalledCloudServers] = useState<Set<string>>(new Set());
|
||||||
|
const [installedRegistryServers, setInstalledRegistryServers] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
// Load server details if a server name is in the URL
|
// Load server details if a server name is in the URL
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadServerDetails = async () => {
|
const loadServerDetails = async () => {
|
||||||
if (serverName) {
|
if (serverName) {
|
||||||
// Determine if it's a cloud or local server based on the current tab
|
// Determine if it's a cloud, local, or registry server based on the current tab
|
||||||
if (currentTab === 'cloud') {
|
if (currentTab === 'cloud') {
|
||||||
// Try to find the server in cloud servers
|
// Try to find the server in cloud servers
|
||||||
const server = cloudServers.find(s => s.name === serverName);
|
const server = cloudServers.find((s) => s.name === serverName);
|
||||||
if (server) {
|
if (server) {
|
||||||
setSelectedCloudServer(server);
|
setSelectedCloudServer(server);
|
||||||
} else {
|
} else {
|
||||||
// If server not found, navigate back to market page
|
// If server not found, navigate back to market page
|
||||||
navigate('/market?tab=cloud');
|
navigate('/market?tab=cloud');
|
||||||
}
|
}
|
||||||
|
} else if (currentTab === 'registry') {
|
||||||
|
console.log('Loading registry server details for:', serverName);
|
||||||
|
// Registry market
|
||||||
|
const serverEntry = await fetchRegistryServerByName(serverName);
|
||||||
|
if (serverEntry) {
|
||||||
|
setSelectedRegistryServer(serverEntry);
|
||||||
|
} else {
|
||||||
|
// If server not found, navigate back to market page
|
||||||
|
navigate('/market?tab=registry');
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Local market
|
// Local market
|
||||||
const server = await fetchLocalServerByName(serverName);
|
const server = await fetchLocalServerByName(serverName);
|
||||||
@@ -97,14 +145,22 @@ const MarketPage: React.FC = () => {
|
|||||||
} else {
|
} else {
|
||||||
setSelectedServer(null);
|
setSelectedServer(null);
|
||||||
setSelectedCloudServer(null);
|
setSelectedCloudServer(null);
|
||||||
|
setSelectedRegistryServer(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
loadServerDetails();
|
loadServerDetails();
|
||||||
}, [serverName, currentTab, cloudServers, fetchLocalServerByName, navigate]);
|
}, [
|
||||||
|
serverName,
|
||||||
|
currentTab,
|
||||||
|
cloudServers,
|
||||||
|
fetchLocalServerByName,
|
||||||
|
fetchRegistryServerByName,
|
||||||
|
navigate,
|
||||||
|
]);
|
||||||
|
|
||||||
// Tab switching handler
|
// Tab switching handler
|
||||||
const switchTab = (tab: 'local' | 'cloud') => {
|
const switchTab = (tab: 'local' | 'cloud' | 'registry') => {
|
||||||
const newSearchParams = new URLSearchParams(searchParams);
|
const newSearchParams = new URLSearchParams(searchParams);
|
||||||
newSearchParams.set('tab', tab);
|
newSearchParams.set('tab', tab);
|
||||||
setSearchParams(newSearchParams);
|
setSearchParams(newSearchParams);
|
||||||
@@ -118,6 +174,8 @@ const MarketPage: React.FC = () => {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (currentTab === 'local') {
|
if (currentTab === 'local') {
|
||||||
searchLocalServers(searchQuery);
|
searchLocalServers(searchQuery);
|
||||||
|
} else if (currentTab === 'registry') {
|
||||||
|
searchRegistryServers(registrySearchQuery);
|
||||||
}
|
}
|
||||||
// Cloud search is not implemented in the original cloud page
|
// Cloud search is not implemented in the original cloud page
|
||||||
};
|
};
|
||||||
@@ -129,18 +187,35 @@ const MarketPage: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleClearFilters = () => {
|
const handleClearFilters = () => {
|
||||||
setSearchQuery('');
|
|
||||||
if (currentTab === 'local') {
|
if (currentTab === 'local') {
|
||||||
|
setSearchQuery('');
|
||||||
filterLocalByCategory('');
|
filterLocalByCategory('');
|
||||||
filterLocalByTag('');
|
filterLocalByTag('');
|
||||||
|
} else if (currentTab === 'registry') {
|
||||||
|
setRegistrySearchQuery('');
|
||||||
|
clearRegistrySearch();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleServerClick = (server: MarketServer | CloudServer) => {
|
const handleServerClick = (server: MarketServer | CloudServer | RegistryServerEntry) => {
|
||||||
if (currentTab === 'cloud') {
|
if (currentTab === 'cloud') {
|
||||||
navigate(`/market/${server.name}?tab=cloud`);
|
const cloudServer = server as CloudServer;
|
||||||
|
navigate(`/market/${cloudServer.name}?tab=cloud`);
|
||||||
|
} else if (currentTab === 'registry') {
|
||||||
|
const registryServer = server as RegistryServerEntry;
|
||||||
|
console.log('Registry server clicked:', registryServer);
|
||||||
|
const serverName = registryServer.server?.name;
|
||||||
|
console.log('Server name extracted:', serverName);
|
||||||
|
if (serverName) {
|
||||||
|
const targetUrl = `/market/${encodeURIComponent(serverName)}?tab=registry`;
|
||||||
|
console.log('Navigating to:', targetUrl);
|
||||||
|
navigate(targetUrl);
|
||||||
|
} else {
|
||||||
|
console.error('Server name is undefined in registry server:', registryServer);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
navigate(`/market/${server.name}?tab=local`);
|
const marketServer = server as MarketServer;
|
||||||
|
navigate(`/market/${marketServer.name}?tab=local`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -167,7 +242,7 @@ const MarketPage: React.FC = () => {
|
|||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
name: server.name,
|
name: server.name,
|
||||||
config: config
|
config: config,
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await apiPost('/servers', payload);
|
const result = await apiPost('/servers', payload);
|
||||||
@@ -179,9 +254,8 @@ const MarketPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update installed servers set
|
// Update installed servers set
|
||||||
setInstalledCloudServers(prev => new Set(prev).add(server.name));
|
setInstalledCloudServers((prev) => new Set(prev).add(server.name));
|
||||||
showToast(t('cloud.installSuccess', { name: server.title || server.name }), 'success');
|
showToast(t('cloud.installSuccess', { name: server.title || server.name }), 'success');
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error installing cloud server:', error);
|
console.error('Error installing cloud server:', error);
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
@@ -191,7 +265,41 @@ const MarketPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCallTool = async (serverName: string, toolName: string, args: Record<string, any>) => {
|
// Handle registry server installation
|
||||||
|
const handleRegistryInstall = async (server: RegistryServerData, config: ServerConfig) => {
|
||||||
|
try {
|
||||||
|
setInstalling(true);
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
name: server.name,
|
||||||
|
config: config,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await apiPost('/servers', payload);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
const errorMessage = result?.message || t('server.addError');
|
||||||
|
showToast(errorMessage, 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update installed servers set
|
||||||
|
setInstalledRegistryServers((prev) => new Set(prev).add(server.name));
|
||||||
|
showToast(t('registry.installSuccess', { name: server.title || server.name }), 'success');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error installing registry server:', error);
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
showToast(t('registry.installError', { error: errorMessage }), 'error');
|
||||||
|
} finally {
|
||||||
|
setInstalling(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCallTool = async (
|
||||||
|
serverName: string,
|
||||||
|
toolName: string,
|
||||||
|
args: Record<string, any>,
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
const result = await callServerTool(serverName, toolName, args);
|
const result = await callServerTool(serverName, toolName, args);
|
||||||
showToast(t('cloud.toolCallSuccess', { toolName }), 'success');
|
showToast(t('cloud.toolCallSuccess', { toolName }), 'success');
|
||||||
@@ -208,13 +316,17 @@ const MarketPage: React.FC = () => {
|
|||||||
|
|
||||||
// Helper function to check if error is MCPRouter API key not configured
|
// Helper function to check if error is MCPRouter API key not configured
|
||||||
const isMCPRouterApiKeyError = (errorMessage: string) => {
|
const isMCPRouterApiKeyError = (errorMessage: string) => {
|
||||||
return errorMessage === 'MCPROUTER_API_KEY_NOT_CONFIGURED' ||
|
return (
|
||||||
errorMessage.toLowerCase().includes('mcprouter api key not configured');
|
errorMessage === 'MCPROUTER_API_KEY_NOT_CONFIGURED' ||
|
||||||
|
errorMessage.toLowerCase().includes('mcprouter api key not configured')
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePageChange = (page: number) => {
|
const handlePageChange = (page: number) => {
|
||||||
if (currentTab === 'local') {
|
if (currentTab === 'local') {
|
||||||
changeLocalPage(page);
|
changeLocalPage(page);
|
||||||
|
} else if (currentTab === 'registry') {
|
||||||
|
changeRegistryPage(page);
|
||||||
} else {
|
} else {
|
||||||
changeCloudPage(page);
|
changeCloudPage(page);
|
||||||
}
|
}
|
||||||
@@ -226,6 +338,8 @@ const MarketPage: React.FC = () => {
|
|||||||
const newValue = parseInt(e.target.value, 10);
|
const newValue = parseInt(e.target.value, 10);
|
||||||
if (currentTab === 'local') {
|
if (currentTab === 'local') {
|
||||||
changeLocalServersPerPage(newValue);
|
changeLocalServersPerPage(newValue);
|
||||||
|
} else if (currentTab === 'registry') {
|
||||||
|
changeRegistryServersPerPage(newValue);
|
||||||
} else {
|
} else {
|
||||||
changeCloudServersPerPage(newValue);
|
changeCloudServersPerPage(newValue);
|
||||||
}
|
}
|
||||||
@@ -259,19 +373,50 @@ const MarketPage: React.FC = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Render registry server detail if selected
|
||||||
|
if (selectedRegistryServer) {
|
||||||
|
return (
|
||||||
|
<RegistryServerDetail
|
||||||
|
serverEntry={selectedRegistryServer}
|
||||||
|
onBack={handleBackToList}
|
||||||
|
onInstall={handleRegistryInstall}
|
||||||
|
installing={installing}
|
||||||
|
isInstalled={installedRegistryServers.has(selectedRegistryServer.server.name)}
|
||||||
|
fetchVersions={fetchRegistryServerVersions}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Get current data based on active tab
|
// Get current data based on active tab
|
||||||
const isLocalTab = currentTab === 'local';
|
const isLocalTab = currentTab === 'local';
|
||||||
const servers = isLocalTab ? localServers : cloudServers;
|
const isRegistryTab = currentTab === 'registry';
|
||||||
const allServers = isLocalTab ? allLocalServers : allCloudServers;
|
const servers = isLocalTab ? localServers : isRegistryTab ? registryServers : cloudServers;
|
||||||
|
const allServers = isLocalTab
|
||||||
|
? allLocalServers
|
||||||
|
: isRegistryTab
|
||||||
|
? allRegistryServers
|
||||||
|
: allCloudServers;
|
||||||
const categories = isLocalTab ? localCategories : [];
|
const categories = isLocalTab ? localCategories : [];
|
||||||
const loading = isLocalTab ? localLoading : cloudLoading;
|
const loading = isLocalTab ? localLoading : isRegistryTab ? registryLoading : cloudLoading;
|
||||||
const error = isLocalTab ? localError : cloudError;
|
const error = isLocalTab ? localError : isRegistryTab ? registryError : cloudError;
|
||||||
const setError = isLocalTab ? setLocalError : setCloudError;
|
const setError = isLocalTab ? setLocalError : isRegistryTab ? setRegistryError : setCloudError;
|
||||||
const selectedCategory = isLocalTab ? selectedLocalCategory : '';
|
const selectedCategory = isLocalTab ? selectedLocalCategory : '';
|
||||||
const selectedTag = isLocalTab ? selectedLocalTag : '';
|
const selectedTag = isLocalTab ? selectedLocalTag : '';
|
||||||
const currentPage = isLocalTab ? localCurrentPage : cloudCurrentPage;
|
const currentPage = isLocalTab
|
||||||
const totalPages = isLocalTab ? localTotalPages : cloudTotalPages;
|
? localCurrentPage
|
||||||
const serversPerPage = isLocalTab ? localServersPerPage : cloudServersPerPage;
|
: isRegistryTab
|
||||||
|
? registryCurrentPage
|
||||||
|
: cloudCurrentPage;
|
||||||
|
const totalPages = isLocalTab
|
||||||
|
? localTotalPages
|
||||||
|
: isRegistryTab
|
||||||
|
? registryTotalPages
|
||||||
|
: cloudTotalPages;
|
||||||
|
const serversPerPage = isLocalTab
|
||||||
|
? localServersPerPage
|
||||||
|
: isRegistryTab
|
||||||
|
? registryServersPerPage
|
||||||
|
: cloudServersPerPage;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -281,13 +426,15 @@ const MarketPage: React.FC = () => {
|
|||||||
<nav className="-mb-px flex space-x-3">
|
<nav className="-mb-px flex space-x-3">
|
||||||
<button
|
<button
|
||||||
onClick={() => switchTab('cloud')}
|
onClick={() => switchTab('cloud')}
|
||||||
className={`py-2 px-1 border-b-2 font-medium text-lg hover:cursor-pointer transition-colors duration-200 ${!isLocalTab
|
className={`py-2 px-1 border-b-2 font-medium text-lg hover:cursor-pointer transition-colors duration-200 ${
|
||||||
? 'border-blue-500 text-blue-600'
|
!isLocalTab && !isRegistryTab
|
||||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
? 'border-blue-500 text-blue-600'
|
||||||
}`}
|
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{t('cloud.title')}
|
{t('cloud.title')}
|
||||||
<span className="text-xs text-gray-400 font-normal ml-1">(
|
<span className="text-xs text-gray-400 font-normal ml-1">
|
||||||
|
(
|
||||||
<a
|
<a
|
||||||
href="https://mcprouter.co"
|
href="https://mcprouter.co"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -301,13 +448,15 @@ const MarketPage: React.FC = () => {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => switchTab('local')}
|
onClick={() => switchTab('local')}
|
||||||
className={`py-2 px-1 border-b-2 font-medium text-lg hover:cursor-pointer transition-colors duration-200 ${isLocalTab
|
className={`py-2 px-1 border-b-2 font-medium text-lg hover:cursor-pointer transition-colors duration-200 ${
|
||||||
? 'border-blue-500 text-blue-600'
|
isLocalTab
|
||||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
? 'border-blue-500 text-blue-600'
|
||||||
}`}
|
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{t('market.title')}
|
{t('market.title')}
|
||||||
<span className="text-xs text-gray-400 font-normal ml-1">(
|
<span className="text-xs text-gray-400 font-normal ml-1">
|
||||||
|
(
|
||||||
<a
|
<a
|
||||||
href="https://mcpm.sh"
|
href="https://mcpm.sh"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -319,6 +468,28 @@ const MarketPage: React.FC = () => {
|
|||||||
)
|
)
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => switchTab('registry')}
|
||||||
|
className={`py-2 px-1 border-b-2 font-medium text-lg hover:cursor-pointer transition-colors duration-200 ${
|
||||||
|
isRegistryTab
|
||||||
|
? 'border-blue-500 text-blue-600'
|
||||||
|
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{t('registry.title')}
|
||||||
|
<span className="text-xs text-gray-400 font-normal ml-1">
|
||||||
|
(
|
||||||
|
<a
|
||||||
|
href="https://registry.modelcontextprotocol.io"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="external-link"
|
||||||
|
>
|
||||||
|
{t('registry.official')}
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -335,8 +506,17 @@ const MarketPage: React.FC = () => {
|
|||||||
onClick={() => setError(null)}
|
onClick={() => setError(null)}
|
||||||
className="text-red-700 hover:text-red-900 transition-colors duration-200"
|
className="text-red-700 hover:text-red-900 transition-colors duration-200"
|
||||||
>
|
>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
<svg
|
||||||
<path fillRule="evenodd" d="M4.293 4.293a1 1 011.414 0L10 8.586l4.293-4.293a1 1 01.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" />
|
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 01.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>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -345,16 +525,24 @@ const MarketPage: React.FC = () => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Search bar for local market only */}
|
{/* Search bar for local market and registry */}
|
||||||
{isLocalTab && (
|
{(isLocalTab || isRegistryTab) && (
|
||||||
<div className="bg-white shadow rounded-lg p-6 mb-6 page-card">
|
<div className="bg-white shadow rounded-lg p-6 mb-6 page-card">
|
||||||
<form onSubmit={handleSearch} className="flex space-x-4 mb-0">
|
<form onSubmit={handleSearch} className="flex space-x-4 mb-0">
|
||||||
<div className="flex-grow">
|
<div className="flex-grow">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={searchQuery}
|
value={isRegistryTab ? registrySearchQuery : searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => {
|
||||||
placeholder={t('market.searchPlaceholder')}
|
if (isRegistryTab) {
|
||||||
|
setRegistrySearchQuery(e.target.value);
|
||||||
|
} else {
|
||||||
|
setSearchQuery(e.target.value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder={
|
||||||
|
isRegistryTab ? t('registry.searchPlaceholder') : t('market.searchPlaceholder')
|
||||||
|
}
|
||||||
className="shadow appearance-none border border-gray-200 rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline form-input"
|
className="shadow appearance-none border border-gray-200 rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline form-input"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -362,15 +550,16 @@ const MarketPage: React.FC = () => {
|
|||||||
type="submit"
|
type="submit"
|
||||||
className="px-4 py-2 bg-blue-100 text-blue-800 rounded hover:bg-blue-200 flex items-center btn-primary transition-all duration-200"
|
className="px-4 py-2 bg-blue-100 text-blue-800 rounded hover:bg-blue-200 flex items-center btn-primary transition-all duration-200"
|
||||||
>
|
>
|
||||||
{t('market.search')}
|
{isRegistryTab ? t('registry.search') : t('market.search')}
|
||||||
</button>
|
</button>
|
||||||
{(searchQuery || selectedCategory || selectedTag) && (
|
{((isLocalTab && (searchQuery || selectedCategory || selectedTag)) ||
|
||||||
|
(isRegistryTab && registrySearchQuery)) && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleClearFilters}
|
onClick={handleClearFilters}
|
||||||
className="border border-gray-300 text-gray-700 font-medium py-2 px-4 rounded hover:bg-gray-50 btn-secondary transition-all duration-200"
|
className="border border-gray-300 text-gray-700 font-medium py-2 px-4 rounded hover:bg-gray-50 btn-secondary transition-all duration-200"
|
||||||
>
|
>
|
||||||
{t('market.clearFilters')}
|
{isRegistryTab ? t('registry.clearFilters') : t('market.clearFilters')}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</form>
|
</form>
|
||||||
@@ -388,7 +577,10 @@ const MarketPage: React.FC = () => {
|
|||||||
<div className="flex justify-between items-center mb-3">
|
<div className="flex justify-between items-center mb-3">
|
||||||
<h3 className="font-medium text-gray-900">{t('market.categories')}</h3>
|
<h3 className="font-medium text-gray-900">{t('market.categories')}</h3>
|
||||||
{selectedCategory && (
|
{selectedCategory && (
|
||||||
<span className="text-xs text-blue-600 cursor-pointer hover:underline transition-colors duration-200" onClick={() => filterLocalByCategory('')}>
|
<span
|
||||||
|
className="text-xs text-blue-600 cursor-pointer hover:underline transition-colors duration-200"
|
||||||
|
onClick={() => filterLocalByCategory('')}
|
||||||
|
>
|
||||||
{t('market.clearCategoryFilter')}
|
{t('market.clearCategoryFilter')}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -398,10 +590,11 @@ const MarketPage: React.FC = () => {
|
|||||||
<button
|
<button
|
||||||
key={category}
|
key={category}
|
||||||
onClick={() => handleCategoryClick(category)}
|
onClick={() => handleCategoryClick(category)}
|
||||||
className={`px-3 py-2 rounded text-sm text-left transition-all duration-200 ${selectedCategory === category
|
className={`px-3 py-2 rounded text-sm text-left transition-all duration-200 ${
|
||||||
? 'bg-blue-100 text-blue-800 font-medium btn-primary'
|
selectedCategory === category
|
||||||
: 'bg-gray-100 text-gray-800 hover:bg-gray-200 btn-secondary'
|
? 'bg-blue-100 text-blue-800 font-medium btn-primary'
|
||||||
}`}
|
: 'bg-gray-100 text-gray-800 hover:bg-gray-200 btn-secondary'
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{category}
|
{category}
|
||||||
</button>
|
</button>
|
||||||
@@ -414,9 +607,25 @@ const MarketPage: React.FC = () => {
|
|||||||
<h3 className="font-medium text-gray-900">{t('market.categories')}</h3>
|
<h3 className="font-medium text-gray-900">{t('market.categories')}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2 items-center py-4 loading-container">
|
<div className="flex flex-col gap-2 items-center py-4 loading-container">
|
||||||
<svg className="animate-spin h-6 w-6 text-blue-500 mb-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
<svg
|
||||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
className="animate-spin h-6 w-6 text-blue-500 mb-2"
|
||||||
<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>
|
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>
|
</svg>
|
||||||
<p className="text-sm text-gray-600">{t('app.loading')}</p>
|
<p className="text-sm text-gray-600">{t('app.loading')}</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -438,61 +647,110 @@ const MarketPage: React.FC = () => {
|
|||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="bg-white shadow rounded-lg p-6 flex items-center justify-center">
|
<div className="bg-white shadow rounded-lg p-6 flex items-center justify-center">
|
||||||
<div className="flex flex-col items-center">
|
<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">
|
<svg
|
||||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
className="animate-spin h-10 w-10 text-blue-500 mb-4"
|
||||||
<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>
|
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>
|
</svg>
|
||||||
<p className="text-gray-600">{t('app.loading')}</p>
|
<p className="text-gray-600">{t('app.loading')}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : servers.length === 0 ? (
|
) : servers.length === 0 ? (
|
||||||
<div className="bg-white shadow rounded-lg p-6">
|
<div className="bg-white shadow rounded-lg p-6">
|
||||||
<p className="text-gray-600">{isLocalTab ? t('market.noServers') : t('cloud.noServers')}</p>
|
<p className="text-gray-600">
|
||||||
|
{isLocalTab
|
||||||
|
? t('market.noServers')
|
||||||
|
: isRegistryTab
|
||||||
|
? t('registry.noServers')
|
||||||
|
: t('cloud.noServers')}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||||
{servers.map((server, index) => (
|
{servers.map((server, index) =>
|
||||||
isLocalTab ? (
|
isLocalTab ? (
|
||||||
<MarketServerCard
|
<MarketServerCard
|
||||||
key={index}
|
key={index}
|
||||||
server={server as MarketServer}
|
server={server as MarketServer}
|
||||||
onClick={handleServerClick}
|
onClick={handleServerClick}
|
||||||
/>
|
/>
|
||||||
|
) : isRegistryTab ? (
|
||||||
|
<RegistryServerCard
|
||||||
|
key={index}
|
||||||
|
serverEntry={server as RegistryServerEntry}
|
||||||
|
onClick={handleServerClick}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<CloudServerCard
|
<CloudServerCard
|
||||||
key={index}
|
key={index}
|
||||||
server={server as CloudServer}
|
server={server as CloudServer}
|
||||||
onClick={handleServerClick}
|
onClick={handleServerClick}
|
||||||
/>
|
/>
|
||||||
)
|
),
|
||||||
))}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-between items-center mb-4">
|
<div className="flex items-center mb-4">
|
||||||
<div className="text-sm text-gray-500">
|
<div className="flex-[2] text-sm text-gray-500">
|
||||||
{isLocalTab ? (
|
{isLocalTab
|
||||||
t('market.showing', {
|
? t('market.showing', {
|
||||||
from: (currentPage - 1) * serversPerPage + 1,
|
from: (currentPage - 1) * serversPerPage + 1,
|
||||||
to: Math.min(currentPage * serversPerPage, allServers.length),
|
to: Math.min(currentPage * serversPerPage, allServers.length),
|
||||||
total: allServers.length
|
total: allServers.length,
|
||||||
})
|
})
|
||||||
|
: isRegistryTab
|
||||||
|
? t('registry.showing', {
|
||||||
|
from: (currentPage - 1) * serversPerPage + 1,
|
||||||
|
to: (currentPage - 1) * serversPerPage + servers.length,
|
||||||
|
total: allServers.length + (registryHasNextPage ? '+' : ''),
|
||||||
|
})
|
||||||
|
: t('cloud.showing', {
|
||||||
|
from: (currentPage - 1) * serversPerPage + 1,
|
||||||
|
to: Math.min(currentPage * serversPerPage, allServers.length),
|
||||||
|
total: allServers.length,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div className="flex-[4] flex justify-center">
|
||||||
|
{isRegistryTab ? (
|
||||||
|
<CursorPagination
|
||||||
|
currentPage={currentPage}
|
||||||
|
hasNextPage={registryHasNextPage}
|
||||||
|
hasPreviousPage={registryHasPreviousPage}
|
||||||
|
onNextPage={goToRegistryNextPage}
|
||||||
|
onPreviousPage={goToRegistryPreviousPage}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
t('cloud.showing', {
|
<Pagination
|
||||||
from: (currentPage - 1) * serversPerPage + 1,
|
currentPage={currentPage}
|
||||||
to: Math.min(currentPage * serversPerPage, allServers.length),
|
totalPages={totalPages}
|
||||||
total: allServers.length
|
onPageChange={handlePageChange}
|
||||||
})
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Pagination
|
<div className="flex-[2] flex items-center justify-end space-x-2">
|
||||||
currentPage={currentPage}
|
|
||||||
totalPages={totalPages}
|
|
||||||
onPageChange={handlePageChange}
|
|
||||||
/>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<label htmlFor="perPage" className="text-sm text-gray-600">
|
<label htmlFor="perPage" className="text-sm text-gray-600">
|
||||||
{isLocalTab ? t('market.perPage') : t('cloud.perPage')}:
|
{isLocalTab
|
||||||
|
? t('market.perPage')
|
||||||
|
: isRegistryTab
|
||||||
|
? t('registry.perPage')
|
||||||
|
: t('cloud.perPage')}
|
||||||
|
:
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
id="perPage"
|
id="perPage"
|
||||||
@@ -507,9 +765,6 @@ const MarketPage: React.FC = () => {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-6">
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -309,3 +309,148 @@ export interface AuthResponse {
|
|||||||
user?: IUser;
|
user?: IUser;
|
||||||
message?: string;
|
message?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Official Registry types (from registry.modelcontextprotocol.io)
|
||||||
|
export interface RegistryVariable {
|
||||||
|
choices?: string[];
|
||||||
|
default?: string;
|
||||||
|
description?: string;
|
||||||
|
format?: string;
|
||||||
|
isRequired?: boolean;
|
||||||
|
isSecret?: boolean;
|
||||||
|
value?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegistryVariables {
|
||||||
|
[key: string]: RegistryVariable;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegistryEnvironmentVariable {
|
||||||
|
choices?: string[];
|
||||||
|
default?: string;
|
||||||
|
description?: string;
|
||||||
|
format?: string;
|
||||||
|
isRequired?: boolean;
|
||||||
|
isSecret?: boolean;
|
||||||
|
name: string;
|
||||||
|
value?: string;
|
||||||
|
variables?: RegistryVariables;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegistryPackageArgument {
|
||||||
|
choices?: string[];
|
||||||
|
default?: string;
|
||||||
|
description?: string;
|
||||||
|
format?: string;
|
||||||
|
isRepeated?: boolean;
|
||||||
|
isRequired?: boolean;
|
||||||
|
isSecret?: boolean;
|
||||||
|
name: string;
|
||||||
|
type?: string;
|
||||||
|
value?: string;
|
||||||
|
valueHint?: string;
|
||||||
|
variables?: RegistryVariables;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegistryTransportHeader {
|
||||||
|
choices?: string[];
|
||||||
|
default?: string;
|
||||||
|
description?: string;
|
||||||
|
format?: string;
|
||||||
|
isRequired?: boolean;
|
||||||
|
isSecret?: boolean;
|
||||||
|
name: string;
|
||||||
|
value?: string;
|
||||||
|
variables?: RegistryVariables;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegistryTransport {
|
||||||
|
headers?: RegistryTransportHeader[];
|
||||||
|
type: string;
|
||||||
|
url?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegistryPackage {
|
||||||
|
environmentVariables?: RegistryEnvironmentVariable[];
|
||||||
|
fileSha256?: string;
|
||||||
|
identifier: string;
|
||||||
|
packageArguments?: RegistryPackageArgument[];
|
||||||
|
registryBaseUrl?: string;
|
||||||
|
registryType: string;
|
||||||
|
runtimeArguments?: RegistryPackageArgument[];
|
||||||
|
runtimeHint?: string;
|
||||||
|
transport?: RegistryTransport;
|
||||||
|
version?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegistryRemote {
|
||||||
|
headers?: RegistryTransportHeader[];
|
||||||
|
type: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegistryRepository {
|
||||||
|
id?: string;
|
||||||
|
source?: string;
|
||||||
|
subfolder?: string;
|
||||||
|
url?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegistryIcon {
|
||||||
|
mimeType: string;
|
||||||
|
sizes?: string[];
|
||||||
|
src: string;
|
||||||
|
theme?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegistryServerData {
|
||||||
|
$schema?: string;
|
||||||
|
_meta?: {
|
||||||
|
'io.modelcontextprotocol.registry/publisher-provided'?: Record<string, any>;
|
||||||
|
};
|
||||||
|
description: string;
|
||||||
|
icons?: RegistryIcon[];
|
||||||
|
name: string;
|
||||||
|
packages?: RegistryPackage[];
|
||||||
|
remotes?: RegistryRemote[];
|
||||||
|
repository?: RegistryRepository;
|
||||||
|
title: string;
|
||||||
|
version: string;
|
||||||
|
websiteUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegistryOfficialMeta {
|
||||||
|
isLatest?: boolean;
|
||||||
|
publishedAt?: string;
|
||||||
|
status?: string;
|
||||||
|
updatedAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegistryServerEntry {
|
||||||
|
_meta?: {
|
||||||
|
'io.modelcontextprotocol.registry/official'?: RegistryOfficialMeta;
|
||||||
|
};
|
||||||
|
server: RegistryServerData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegistryMetadata {
|
||||||
|
count: number;
|
||||||
|
nextCursor?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegistryServersResponse {
|
||||||
|
metadata: RegistryMetadata;
|
||||||
|
servers: RegistryServerEntry[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegistryServerVersionsResponse {
|
||||||
|
metadata: RegistryMetadata;
|
||||||
|
servers: RegistryServerEntry[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegistryServerVersionResponse {
|
||||||
|
_meta?: {
|
||||||
|
'io.modelcontextprotocol.registry/official'?: RegistryOfficialMeta;
|
||||||
|
};
|
||||||
|
server: RegistryServerData;
|
||||||
|
}
|
||||||
|
|||||||
@@ -211,7 +211,15 @@
|
|||||||
"dismiss": "Dismiss",
|
"dismiss": "Dismiss",
|
||||||
"github": "GitHub",
|
"github": "GitHub",
|
||||||
"wechat": "WeChat",
|
"wechat": "WeChat",
|
||||||
"discord": "Discord"
|
"discord": "Discord",
|
||||||
|
"required": "Required",
|
||||||
|
"secret": "Secret",
|
||||||
|
"default": "Default",
|
||||||
|
"value": "Value",
|
||||||
|
"type": "Type",
|
||||||
|
"repeated": "Repeated",
|
||||||
|
"valueHint": "Value Hint",
|
||||||
|
"choices": "Choices"
|
||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"dashboard": "Dashboard",
|
"dashboard": "Dashboard",
|
||||||
@@ -401,6 +409,41 @@
|
|||||||
"installSuccess": "Server {{name}} installed successfully",
|
"installSuccess": "Server {{name}} installed successfully",
|
||||||
"installError": "Failed to install server: {{error}}"
|
"installError": "Failed to install server: {{error}}"
|
||||||
},
|
},
|
||||||
|
"registry": {
|
||||||
|
"title": "Registry",
|
||||||
|
"official": "Official",
|
||||||
|
"latest": "Latest",
|
||||||
|
"description": "Description",
|
||||||
|
"website": "Website",
|
||||||
|
"repository": "Repository",
|
||||||
|
"packages": "Packages",
|
||||||
|
"package": "package",
|
||||||
|
"remotes": "Remotes",
|
||||||
|
"remote": "remote",
|
||||||
|
"published": "Published",
|
||||||
|
"updated": "Updated",
|
||||||
|
"install": "Install",
|
||||||
|
"installing": "Installing...",
|
||||||
|
"installed": "Installed",
|
||||||
|
"installServer": "Install {{name}}",
|
||||||
|
"installSuccess": "Server {{name}} installed successfully",
|
||||||
|
"installError": "Failed to install server: {{error}}",
|
||||||
|
"noDescription": "No description available",
|
||||||
|
"viewDetails": "View Details",
|
||||||
|
"backToList": "Back to Registry",
|
||||||
|
"search": "Search",
|
||||||
|
"searchPlaceholder": "Search registry servers by name",
|
||||||
|
"clearFilters": "Clear",
|
||||||
|
"noServers": "No registry servers found",
|
||||||
|
"fetchError": "Error fetching registry servers",
|
||||||
|
"serverNotFound": "Registry server not found",
|
||||||
|
"showing": "Showing {{from}}-{{to}} of {{total}} registry servers",
|
||||||
|
"perPage": "Per page",
|
||||||
|
"environmentVariables": "Environment Variables",
|
||||||
|
"packageArguments": "Package Arguments",
|
||||||
|
"runtimeArguments": "Runtime Arguments",
|
||||||
|
"headers": "Headers"
|
||||||
|
},
|
||||||
"tool": {
|
"tool": {
|
||||||
"run": "Run",
|
"run": "Run",
|
||||||
"running": "Running...",
|
"running": "Running...",
|
||||||
|
|||||||
@@ -211,7 +211,15 @@
|
|||||||
"dismiss": "Rejeter",
|
"dismiss": "Rejeter",
|
||||||
"github": "GitHub",
|
"github": "GitHub",
|
||||||
"wechat": "WeChat",
|
"wechat": "WeChat",
|
||||||
"discord": "Discord"
|
"discord": "Discord",
|
||||||
|
"required": "Requis",
|
||||||
|
"secret": "Secret",
|
||||||
|
"default": "Défaut",
|
||||||
|
"value": "Valeur",
|
||||||
|
"type": "Type",
|
||||||
|
"repeated": "Répété",
|
||||||
|
"valueHint": "Indice de valeur",
|
||||||
|
"choices": "Choix"
|
||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"dashboard": "Tableau de bord",
|
"dashboard": "Tableau de bord",
|
||||||
@@ -401,6 +409,41 @@
|
|||||||
"installSuccess": "Serveur {{name}} installé avec succès",
|
"installSuccess": "Serveur {{name}} installé avec succès",
|
||||||
"installError": "Échec de l'installation du serveur : {{error}}"
|
"installError": "Échec de l'installation du serveur : {{error}}"
|
||||||
},
|
},
|
||||||
|
"registry": {
|
||||||
|
"title": "Registre",
|
||||||
|
"official": "Officiel",
|
||||||
|
"latest": "Dernière version",
|
||||||
|
"description": "Description",
|
||||||
|
"website": "Site web",
|
||||||
|
"repository": "Dépôt",
|
||||||
|
"packages": "Paquets",
|
||||||
|
"package": "paquet",
|
||||||
|
"remotes": "Services distants",
|
||||||
|
"remote": "service distant",
|
||||||
|
"published": "Publié",
|
||||||
|
"updated": "Mis à jour",
|
||||||
|
"install": "Installer",
|
||||||
|
"installing": "Installation...",
|
||||||
|
"installed": "Installé",
|
||||||
|
"installServer": "Installer {{name}}",
|
||||||
|
"installSuccess": "Serveur {{name}} installé avec succès",
|
||||||
|
"installError": "Échec de l'installation du serveur : {{error}}",
|
||||||
|
"noDescription": "Aucune description disponible",
|
||||||
|
"viewDetails": "Voir les détails",
|
||||||
|
"backToList": "Retour au registre",
|
||||||
|
"search": "Rechercher",
|
||||||
|
"searchPlaceholder": "Rechercher des serveurs par nom",
|
||||||
|
"clearFilters": "Effacer",
|
||||||
|
"noServers": "Aucun serveur trouvé dans le registre",
|
||||||
|
"fetchError": "Erreur lors de la récupération des serveurs du registre",
|
||||||
|
"serverNotFound": "Serveur du registre non trouvé",
|
||||||
|
"showing": "Affichage de {{from}}-{{to}} sur {{total}} serveurs du registre",
|
||||||
|
"perPage": "Par page",
|
||||||
|
"environmentVariables": "Variables d'environnement",
|
||||||
|
"packageArguments": "Arguments du paquet",
|
||||||
|
"runtimeArguments": "Arguments d'exécution",
|
||||||
|
"headers": "En-têtes"
|
||||||
|
},
|
||||||
"tool": {
|
"tool": {
|
||||||
"run": "Exécuter",
|
"run": "Exécuter",
|
||||||
"running": "Exécution en cours...",
|
"running": "Exécution en cours...",
|
||||||
|
|||||||
@@ -212,7 +212,15 @@
|
|||||||
"dismiss": "忽略",
|
"dismiss": "忽略",
|
||||||
"github": "GitHub",
|
"github": "GitHub",
|
||||||
"wechat": "微信",
|
"wechat": "微信",
|
||||||
"discord": "Discord"
|
"discord": "Discord",
|
||||||
|
"required": "必填",
|
||||||
|
"secret": "敏感",
|
||||||
|
"default": "默认值",
|
||||||
|
"value": "值",
|
||||||
|
"type": "类型",
|
||||||
|
"repeated": "可重复",
|
||||||
|
"valueHint": "值提示",
|
||||||
|
"choices": "可选值"
|
||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"dashboard": "仪表盘",
|
"dashboard": "仪表盘",
|
||||||
@@ -402,6 +410,41 @@
|
|||||||
"installSuccess": "服务器 {{name}} 安装成功",
|
"installSuccess": "服务器 {{name}} 安装成功",
|
||||||
"installError": "安装服务器失败:{{error}}"
|
"installError": "安装服务器失败:{{error}}"
|
||||||
},
|
},
|
||||||
|
"registry": {
|
||||||
|
"title": "注册中心",
|
||||||
|
"official": "官方",
|
||||||
|
"latest": "最新版本",
|
||||||
|
"description": "描述",
|
||||||
|
"website": "网站",
|
||||||
|
"repository": "代码仓库",
|
||||||
|
"packages": "安装包",
|
||||||
|
"package": "安装包",
|
||||||
|
"remotes": "远程服务",
|
||||||
|
"remote": "远程服务",
|
||||||
|
"published": "发布时间",
|
||||||
|
"updated": "更新时间",
|
||||||
|
"install": "安装",
|
||||||
|
"installing": "安装中...",
|
||||||
|
"installed": "已安装",
|
||||||
|
"installServer": "安装 {{name}}",
|
||||||
|
"installSuccess": "服务器 {{name}} 安装成功",
|
||||||
|
"installError": "安装服务器失败:{{error}}",
|
||||||
|
"noDescription": "无描述信息",
|
||||||
|
"viewDetails": "查看详情",
|
||||||
|
"backToList": "返回注册中心",
|
||||||
|
"search": "搜索",
|
||||||
|
"searchPlaceholder": "按名称搜索注册中心服务器",
|
||||||
|
"clearFilters": "清除",
|
||||||
|
"noServers": "未找到注册中心服务器",
|
||||||
|
"fetchError": "获取注册中心服务器失败",
|
||||||
|
"serverNotFound": "未找到注册中心服务器",
|
||||||
|
"showing": "显示 {{from}}-{{to}}/{{total}} 个注册中心服务器",
|
||||||
|
"perPage": "每页显示",
|
||||||
|
"environmentVariables": "环境变量",
|
||||||
|
"packageArguments": "安装包参数",
|
||||||
|
"runtimeArguments": "运行时参数",
|
||||||
|
"headers": "请求头"
|
||||||
|
},
|
||||||
"tool": {
|
"tool": {
|
||||||
"run": "运行",
|
"run": "运行",
|
||||||
"running": "运行中...",
|
"running": "运行中...",
|
||||||
|
|||||||
53
pnpm-lock.yaml
generated
53
pnpm-lock.yaml
generated
@@ -693,92 +693,78 @@ packages:
|
|||||||
resolution: {integrity: sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==}
|
resolution: {integrity: sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@img/sharp-libvips-linux-arm@1.2.0':
|
'@img/sharp-libvips-linux-arm@1.2.0':
|
||||||
resolution: {integrity: sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==}
|
resolution: {integrity: sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@img/sharp-libvips-linux-ppc64@1.2.0':
|
'@img/sharp-libvips-linux-ppc64@1.2.0':
|
||||||
resolution: {integrity: sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==}
|
resolution: {integrity: sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==}
|
||||||
cpu: [ppc64]
|
cpu: [ppc64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@img/sharp-libvips-linux-s390x@1.2.0':
|
'@img/sharp-libvips-linux-s390x@1.2.0':
|
||||||
resolution: {integrity: sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==}
|
resolution: {integrity: sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==}
|
||||||
cpu: [s390x]
|
cpu: [s390x]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@img/sharp-libvips-linux-x64@1.2.0':
|
'@img/sharp-libvips-linux-x64@1.2.0':
|
||||||
resolution: {integrity: sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==}
|
resolution: {integrity: sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@img/sharp-libvips-linuxmusl-arm64@1.2.0':
|
'@img/sharp-libvips-linuxmusl-arm64@1.2.0':
|
||||||
resolution: {integrity: sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==}
|
resolution: {integrity: sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@img/sharp-libvips-linuxmusl-x64@1.2.0':
|
'@img/sharp-libvips-linuxmusl-x64@1.2.0':
|
||||||
resolution: {integrity: sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==}
|
resolution: {integrity: sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@img/sharp-linux-arm64@0.34.3':
|
'@img/sharp-linux-arm64@0.34.3':
|
||||||
resolution: {integrity: sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==}
|
resolution: {integrity: sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@img/sharp-linux-arm@0.34.3':
|
'@img/sharp-linux-arm@0.34.3':
|
||||||
resolution: {integrity: sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==}
|
resolution: {integrity: sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@img/sharp-linux-ppc64@0.34.3':
|
'@img/sharp-linux-ppc64@0.34.3':
|
||||||
resolution: {integrity: sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==}
|
resolution: {integrity: sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
cpu: [ppc64]
|
cpu: [ppc64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@img/sharp-linux-s390x@0.34.3':
|
'@img/sharp-linux-s390x@0.34.3':
|
||||||
resolution: {integrity: sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==}
|
resolution: {integrity: sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
cpu: [s390x]
|
cpu: [s390x]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@img/sharp-linux-x64@0.34.3':
|
'@img/sharp-linux-x64@0.34.3':
|
||||||
resolution: {integrity: sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==}
|
resolution: {integrity: sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@img/sharp-linuxmusl-arm64@0.34.3':
|
'@img/sharp-linuxmusl-arm64@0.34.3':
|
||||||
resolution: {integrity: sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==}
|
resolution: {integrity: sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@img/sharp-linuxmusl-x64@0.34.3':
|
'@img/sharp-linuxmusl-x64@0.34.3':
|
||||||
resolution: {integrity: sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==}
|
resolution: {integrity: sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@img/sharp-wasm32@0.34.3':
|
'@img/sharp-wasm32@0.34.3':
|
||||||
resolution: {integrity: sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==}
|
resolution: {integrity: sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==}
|
||||||
@@ -970,28 +956,24 @@ packages:
|
|||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@next/swc-linux-arm64-musl@15.5.2':
|
'@next/swc-linux-arm64-musl@15.5.2':
|
||||||
resolution: {integrity: sha512-s6N8k8dF9YGc5T01UPQ08yxsK6fUow5gG1/axWc1HVVBYQBgOjca4oUZF7s4p+kwhkB1bDSGR8QznWrFZ/Rt5g==}
|
resolution: {integrity: sha512-s6N8k8dF9YGc5T01UPQ08yxsK6fUow5gG1/axWc1HVVBYQBgOjca4oUZF7s4p+kwhkB1bDSGR8QznWrFZ/Rt5g==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@next/swc-linux-x64-gnu@15.5.2':
|
'@next/swc-linux-x64-gnu@15.5.2':
|
||||||
resolution: {integrity: sha512-o1RV/KOODQh6dM6ZRJGZbc+MOAHww33Vbs5JC9Mp1gDk8cpEO+cYC/l7rweiEalkSm5/1WGa4zY7xrNwObN4+Q==}
|
resolution: {integrity: sha512-o1RV/KOODQh6dM6ZRJGZbc+MOAHww33Vbs5JC9Mp1gDk8cpEO+cYC/l7rweiEalkSm5/1WGa4zY7xrNwObN4+Q==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@next/swc-linux-x64-musl@15.5.2':
|
'@next/swc-linux-x64-musl@15.5.2':
|
||||||
resolution: {integrity: sha512-/VUnh7w8RElYZ0IV83nUcP/J4KJ6LLYliiBIri3p3aW2giF+PAVgZb6mk8jbQSB3WlTai8gEmCAr7kptFa1H6g==}
|
resolution: {integrity: sha512-/VUnh7w8RElYZ0IV83nUcP/J4KJ6LLYliiBIri3p3aW2giF+PAVgZb6mk8jbQSB3WlTai8gEmCAr7kptFa1H6g==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@next/swc-win32-arm64-msvc@15.5.2':
|
'@next/swc-win32-arm64-msvc@15.5.2':
|
||||||
resolution: {integrity: sha512-sMPyTvRcNKXseNQ/7qRfVRLa0VhR0esmQ29DD6pqvG71+JdVnESJaHPA8t7bc67KD5spP3+DOCNLhqlEI2ZgQg==}
|
resolution: {integrity: sha512-sMPyTvRcNKXseNQ/7qRfVRLa0VhR0esmQ29DD6pqvG71+JdVnESJaHPA8t7bc67KD5spP3+DOCNLhqlEI2ZgQg==}
|
||||||
@@ -1209,67 +1191,56 @@ packages:
|
|||||||
resolution: {integrity: sha512-54v4okehwl5TaSIkpp97rAHGp7t3ghinRd/vyC1iXqXMfjYUTm7TfYmCzXDoHUPTTf36L8pr0E7YsD3CfB3ZDg==}
|
resolution: {integrity: sha512-54v4okehwl5TaSIkpp97rAHGp7t3ghinRd/vyC1iXqXMfjYUTm7TfYmCzXDoHUPTTf36L8pr0E7YsD3CfB3ZDg==}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-arm-musleabihf@4.50.1':
|
'@rollup/rollup-linux-arm-musleabihf@4.50.1':
|
||||||
resolution: {integrity: sha512-p/LaFyajPN/0PUHjv8TNyxLiA7RwmDoVY3flXHPSzqrGcIp/c2FjwPPP5++u87DGHtw+5kSH5bCJz0mvXngYxw==}
|
resolution: {integrity: sha512-p/LaFyajPN/0PUHjv8TNyxLiA7RwmDoVY3flXHPSzqrGcIp/c2FjwPPP5++u87DGHtw+5kSH5bCJz0mvXngYxw==}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-arm64-gnu@4.50.1':
|
'@rollup/rollup-linux-arm64-gnu@4.50.1':
|
||||||
resolution: {integrity: sha512-2AbMhFFkTo6Ptna1zO7kAXXDLi7H9fGTbVaIq2AAYO7yzcAsuTNWPHhb2aTA6GPiP+JXh85Y8CiS54iZoj4opw==}
|
resolution: {integrity: sha512-2AbMhFFkTo6Ptna1zO7kAXXDLi7H9fGTbVaIq2AAYO7yzcAsuTNWPHhb2aTA6GPiP+JXh85Y8CiS54iZoj4opw==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-arm64-musl@4.50.1':
|
'@rollup/rollup-linux-arm64-musl@4.50.1':
|
||||||
resolution: {integrity: sha512-Cgef+5aZwuvesQNw9eX7g19FfKX5/pQRIyhoXLCiBOrWopjo7ycfB292TX9MDcDijiuIJlx1IzJz3IoCPfqs9w==}
|
resolution: {integrity: sha512-Cgef+5aZwuvesQNw9eX7g19FfKX5/pQRIyhoXLCiBOrWopjo7ycfB292TX9MDcDijiuIJlx1IzJz3IoCPfqs9w==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-loongarch64-gnu@4.50.1':
|
'@rollup/rollup-linux-loongarch64-gnu@4.50.1':
|
||||||
resolution: {integrity: sha512-RPhTwWMzpYYrHrJAS7CmpdtHNKtt2Ueo+BlLBjfZEhYBhK00OsEqM08/7f+eohiF6poe0YRDDd8nAvwtE/Y62Q==}
|
resolution: {integrity: sha512-RPhTwWMzpYYrHrJAS7CmpdtHNKtt2Ueo+BlLBjfZEhYBhK00OsEqM08/7f+eohiF6poe0YRDDd8nAvwtE/Y62Q==}
|
||||||
cpu: [loong64]
|
cpu: [loong64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-ppc64-gnu@4.50.1':
|
'@rollup/rollup-linux-ppc64-gnu@4.50.1':
|
||||||
resolution: {integrity: sha512-eSGMVQw9iekut62O7eBdbiccRguuDgiPMsw++BVUg+1K7WjZXHOg/YOT9SWMzPZA+w98G+Fa1VqJgHZOHHnY0Q==}
|
resolution: {integrity: sha512-eSGMVQw9iekut62O7eBdbiccRguuDgiPMsw++BVUg+1K7WjZXHOg/YOT9SWMzPZA+w98G+Fa1VqJgHZOHHnY0Q==}
|
||||||
cpu: [ppc64]
|
cpu: [ppc64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-riscv64-gnu@4.50.1':
|
'@rollup/rollup-linux-riscv64-gnu@4.50.1':
|
||||||
resolution: {integrity: sha512-S208ojx8a4ciIPrLgazF6AgdcNJzQE4+S9rsmOmDJkusvctii+ZvEuIC4v/xFqzbuP8yDjn73oBlNDgF6YGSXQ==}
|
resolution: {integrity: sha512-S208ojx8a4ciIPrLgazF6AgdcNJzQE4+S9rsmOmDJkusvctii+ZvEuIC4v/xFqzbuP8yDjn73oBlNDgF6YGSXQ==}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-riscv64-musl@4.50.1':
|
'@rollup/rollup-linux-riscv64-musl@4.50.1':
|
||||||
resolution: {integrity: sha512-3Ag8Ls1ggqkGUvSZWYcdgFwriy2lWo+0QlYgEFra/5JGtAd6C5Hw59oojx1DeqcA2Wds2ayRgvJ4qxVTzCHgzg==}
|
resolution: {integrity: sha512-3Ag8Ls1ggqkGUvSZWYcdgFwriy2lWo+0QlYgEFra/5JGtAd6C5Hw59oojx1DeqcA2Wds2ayRgvJ4qxVTzCHgzg==}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-s390x-gnu@4.50.1':
|
'@rollup/rollup-linux-s390x-gnu@4.50.1':
|
||||||
resolution: {integrity: sha512-t9YrKfaxCYe7l7ldFERE1BRg/4TATxIg+YieHQ966jwvo7ddHJxPj9cNFWLAzhkVsbBvNA4qTbPVNsZKBO4NSg==}
|
resolution: {integrity: sha512-t9YrKfaxCYe7l7ldFERE1BRg/4TATxIg+YieHQ966jwvo7ddHJxPj9cNFWLAzhkVsbBvNA4qTbPVNsZKBO4NSg==}
|
||||||
cpu: [s390x]
|
cpu: [s390x]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-x64-gnu@4.50.1':
|
'@rollup/rollup-linux-x64-gnu@4.50.1':
|
||||||
resolution: {integrity: sha512-MCgtFB2+SVNuQmmjHf+wfI4CMxy3Tk8XjA5Z//A0AKD7QXUYFMQcns91K6dEHBvZPCnhJSyDWLApk40Iq/H3tA==}
|
resolution: {integrity: sha512-MCgtFB2+SVNuQmmjHf+wfI4CMxy3Tk8XjA5Z//A0AKD7QXUYFMQcns91K6dEHBvZPCnhJSyDWLApk40Iq/H3tA==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-x64-musl@4.50.1':
|
'@rollup/rollup-linux-x64-musl@4.50.1':
|
||||||
resolution: {integrity: sha512-nEvqG+0jeRmqaUMuwzlfMKwcIVffy/9KGbAGyoa26iu6eSngAYQ512bMXuqqPrlTyfqdlB9FVINs93j534UJrg==}
|
resolution: {integrity: sha512-nEvqG+0jeRmqaUMuwzlfMKwcIVffy/9KGbAGyoa26iu6eSngAYQ512bMXuqqPrlTyfqdlB9FVINs93j534UJrg==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@rollup/rollup-openharmony-arm64@4.50.1':
|
'@rollup/rollup-openharmony-arm64@4.50.1':
|
||||||
resolution: {integrity: sha512-RDsLm+phmT3MJd9SNxA9MNuEAO/J2fhW8GXk62G/B4G7sLVumNFbRwDL6v5NrESb48k+QMqdGbHgEtfU0LCpbA==}
|
resolution: {integrity: sha512-RDsLm+phmT3MJd9SNxA9MNuEAO/J2fhW8GXk62G/B4G7sLVumNFbRwDL6v5NrESb48k+QMqdGbHgEtfU0LCpbA==}
|
||||||
@@ -1330,28 +1301,24 @@ packages:
|
|||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@swc/core-linux-arm64-musl@1.13.5':
|
'@swc/core-linux-arm64-musl@1.13.5':
|
||||||
resolution: {integrity: sha512-9+ZxFN5GJag4CnYnq6apKTnnezpfJhCumyz0504/JbHLo+Ue+ZtJnf3RhyA9W9TINtLE0bC4hKpWi8ZKoETyOQ==}
|
resolution: {integrity: sha512-9+ZxFN5GJag4CnYnq6apKTnnezpfJhCumyz0504/JbHLo+Ue+ZtJnf3RhyA9W9TINtLE0bC4hKpWi8ZKoETyOQ==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@swc/core-linux-x64-gnu@1.13.5':
|
'@swc/core-linux-x64-gnu@1.13.5':
|
||||||
resolution: {integrity: sha512-WD530qvHrki8Ywt/PloKUjaRKgstQqNGvmZl54g06kA+hqtSE2FTG9gngXr3UJxYu/cNAjJYiBifm7+w4nbHbA==}
|
resolution: {integrity: sha512-WD530qvHrki8Ywt/PloKUjaRKgstQqNGvmZl54g06kA+hqtSE2FTG9gngXr3UJxYu/cNAjJYiBifm7+w4nbHbA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@swc/core-linux-x64-musl@1.13.5':
|
'@swc/core-linux-x64-musl@1.13.5':
|
||||||
resolution: {integrity: sha512-Luj8y4OFYx4DHNQTWjdIuKTq2f5k6uSXICqx+FSabnXptaOBAbJHNbHT/06JZh6NRUouaf0mYXN0mcsqvkhd7Q==}
|
resolution: {integrity: sha512-Luj8y4OFYx4DHNQTWjdIuKTq2f5k6uSXICqx+FSabnXptaOBAbJHNbHT/06JZh6NRUouaf0mYXN0mcsqvkhd7Q==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@swc/core-win32-arm64-msvc@1.13.5':
|
'@swc/core-win32-arm64-msvc@1.13.5':
|
||||||
resolution: {integrity: sha512-cZ6UpumhF9SDJvv4DA2fo9WIzlNFuKSkZpZmPG1c+4PFSEMy5DFOjBSllCvnqihCabzXzpn6ykCwBmHpy31vQw==}
|
resolution: {integrity: sha512-cZ6UpumhF9SDJvv4DA2fo9WIzlNFuKSkZpZmPG1c+4PFSEMy5DFOjBSllCvnqihCabzXzpn6ykCwBmHpy31vQw==}
|
||||||
@@ -1471,56 +1438,48 @@ packages:
|
|||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@tailwindcss/oxide-linux-arm64-gnu@4.1.14':
|
'@tailwindcss/oxide-linux-arm64-gnu@4.1.14':
|
||||||
resolution: {integrity: sha512-qaEy0dIZ6d9vyLnmeg24yzA8XuEAD9WjpM5nIM1sUgQ/Zv7cVkharPDQcmm/t/TvXoKo/0knI3me3AGfdx6w1w==}
|
resolution: {integrity: sha512-qaEy0dIZ6d9vyLnmeg24yzA8XuEAD9WjpM5nIM1sUgQ/Zv7cVkharPDQcmm/t/TvXoKo/0knI3me3AGfdx6w1w==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@tailwindcss/oxide-linux-arm64-musl@4.1.12':
|
'@tailwindcss/oxide-linux-arm64-musl@4.1.12':
|
||||||
resolution: {integrity: sha512-V8pAM3s8gsrXcCv6kCHSuwyb/gPsd863iT+v1PGXC4fSL/OJqsKhfK//v8P+w9ThKIoqNbEnsZqNy+WDnwQqCA==}
|
resolution: {integrity: sha512-V8pAM3s8gsrXcCv6kCHSuwyb/gPsd863iT+v1PGXC4fSL/OJqsKhfK//v8P+w9ThKIoqNbEnsZqNy+WDnwQqCA==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@tailwindcss/oxide-linux-arm64-musl@4.1.14':
|
'@tailwindcss/oxide-linux-arm64-musl@4.1.14':
|
||||||
resolution: {integrity: sha512-ISZjT44s59O8xKsPEIesiIydMG/sCXoMBCqsphDm/WcbnuWLxxb+GcvSIIA5NjUw6F8Tex7s5/LM2yDy8RqYBQ==}
|
resolution: {integrity: sha512-ISZjT44s59O8xKsPEIesiIydMG/sCXoMBCqsphDm/WcbnuWLxxb+GcvSIIA5NjUw6F8Tex7s5/LM2yDy8RqYBQ==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@tailwindcss/oxide-linux-x64-gnu@4.1.12':
|
'@tailwindcss/oxide-linux-x64-gnu@4.1.12':
|
||||||
resolution: {integrity: sha512-xYfqYLjvm2UQ3TZggTGrwxjYaLB62b1Wiysw/YE3Yqbh86sOMoTn0feF98PonP7LtjsWOWcXEbGqDL7zv0uW8Q==}
|
resolution: {integrity: sha512-xYfqYLjvm2UQ3TZggTGrwxjYaLB62b1Wiysw/YE3Yqbh86sOMoTn0feF98PonP7LtjsWOWcXEbGqDL7zv0uW8Q==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@tailwindcss/oxide-linux-x64-gnu@4.1.14':
|
'@tailwindcss/oxide-linux-x64-gnu@4.1.14':
|
||||||
resolution: {integrity: sha512-02c6JhLPJj10L2caH4U0zF8Hji4dOeahmuMl23stk0MU1wfd1OraE7rOloidSF8W5JTHkFdVo/O7uRUJJnUAJg==}
|
resolution: {integrity: sha512-02c6JhLPJj10L2caH4U0zF8Hji4dOeahmuMl23stk0MU1wfd1OraE7rOloidSF8W5JTHkFdVo/O7uRUJJnUAJg==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@tailwindcss/oxide-linux-x64-musl@4.1.12':
|
'@tailwindcss/oxide-linux-x64-musl@4.1.12':
|
||||||
resolution: {integrity: sha512-ha0pHPamN+fWZY7GCzz5rKunlv9L5R8kdh+YNvP5awe3LtuXb5nRi/H27GeL2U+TdhDOptU7T6Is7mdwh5Ar3A==}
|
resolution: {integrity: sha512-ha0pHPamN+fWZY7GCzz5rKunlv9L5R8kdh+YNvP5awe3LtuXb5nRi/H27GeL2U+TdhDOptU7T6Is7mdwh5Ar3A==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@tailwindcss/oxide-linux-x64-musl@4.1.14':
|
'@tailwindcss/oxide-linux-x64-musl@4.1.14':
|
||||||
resolution: {integrity: sha512-TNGeLiN1XS66kQhxHG/7wMeQDOoL0S33x9BgmydbrWAb9Qw0KYdd8o1ifx4HOGDWhVmJ+Ul+JQ7lyknQFilO3Q==}
|
resolution: {integrity: sha512-TNGeLiN1XS66kQhxHG/7wMeQDOoL0S33x9BgmydbrWAb9Qw0KYdd8o1ifx4HOGDWhVmJ+Ul+JQ7lyknQFilO3Q==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@tailwindcss/oxide-wasm32-wasi@4.1.12':
|
'@tailwindcss/oxide-wasm32-wasi@4.1.12':
|
||||||
resolution: {integrity: sha512-4tSyu3dW+ktzdEpuk6g49KdEangu3eCYoqPhWNsZgUhyegEda3M9rG0/j1GV/JjVVsj+lG7jWAyrTlLzd/WEBg==}
|
resolution: {integrity: sha512-4tSyu3dW+ktzdEpuk6g49KdEangu3eCYoqPhWNsZgUhyegEda3M9rG0/j1GV/JjVVsj+lG7jWAyrTlLzd/WEBg==}
|
||||||
@@ -1857,49 +1816,41 @@ packages:
|
|||||||
resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==}
|
resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@unrs/resolver-binding-linux-arm64-musl@1.11.1':
|
'@unrs/resolver-binding-linux-arm64-musl@1.11.1':
|
||||||
resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==}
|
resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@unrs/resolver-binding-linux-ppc64-gnu@1.11.1':
|
'@unrs/resolver-binding-linux-ppc64-gnu@1.11.1':
|
||||||
resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==}
|
resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==}
|
||||||
cpu: [ppc64]
|
cpu: [ppc64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@unrs/resolver-binding-linux-riscv64-gnu@1.11.1':
|
'@unrs/resolver-binding-linux-riscv64-gnu@1.11.1':
|
||||||
resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==}
|
resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@unrs/resolver-binding-linux-riscv64-musl@1.11.1':
|
'@unrs/resolver-binding-linux-riscv64-musl@1.11.1':
|
||||||
resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==}
|
resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@unrs/resolver-binding-linux-s390x-gnu@1.11.1':
|
'@unrs/resolver-binding-linux-s390x-gnu@1.11.1':
|
||||||
resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==}
|
resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==}
|
||||||
cpu: [s390x]
|
cpu: [s390x]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@unrs/resolver-binding-linux-x64-gnu@1.11.1':
|
'@unrs/resolver-binding-linux-x64-gnu@1.11.1':
|
||||||
resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==}
|
resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@unrs/resolver-binding-linux-x64-musl@1.11.1':
|
'@unrs/resolver-binding-linux-x64-musl@1.11.1':
|
||||||
resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==}
|
resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@unrs/resolver-binding-wasm32-wasi@1.11.1':
|
'@unrs/resolver-binding-wasm32-wasi@1.11.1':
|
||||||
resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==}
|
resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==}
|
||||||
@@ -3246,28 +3197,24 @@ packages:
|
|||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
lightningcss-linux-arm64-musl@1.30.1:
|
lightningcss-linux-arm64-musl@1.30.1:
|
||||||
resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==}
|
resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
lightningcss-linux-x64-gnu@1.30.1:
|
lightningcss-linux-x64-gnu@1.30.1:
|
||||||
resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==}
|
resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
lightningcss-linux-x64-musl@1.30.1:
|
lightningcss-linux-x64-musl@1.30.1:
|
||||||
resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==}
|
resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
lightningcss-win32-arm64-msvc@1.30.1:
|
lightningcss-win32-arm64-msvc@1.30.1:
|
||||||
resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==}
|
resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==}
|
||||||
|
|||||||
169
src/controllers/registryController.ts
Normal file
169
src/controllers/registryController.ts
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { ApiResponse } from '../types/index.js';
|
||||||
|
|
||||||
|
const REGISTRY_BASE_URL = 'https://registry.modelcontextprotocol.io/v0.1';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all MCP servers from the official registry
|
||||||
|
* Proxies the request to avoid CORS issues in the frontend
|
||||||
|
* Supports cursor-based pagination
|
||||||
|
*/
|
||||||
|
export const getAllRegistryServers = async (req: Request, res: Response): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const { cursor, limit, search } = req.query;
|
||||||
|
|
||||||
|
// Build URL with query parameters
|
||||||
|
const url = new URL(`${REGISTRY_BASE_URL}/servers`);
|
||||||
|
if (cursor && typeof cursor === 'string') {
|
||||||
|
url.searchParams.append('cursor', cursor);
|
||||||
|
}
|
||||||
|
if (limit && typeof limit === 'string') {
|
||||||
|
const limitNum = parseInt(limit, 10);
|
||||||
|
if (!isNaN(limitNum) && limitNum > 0) {
|
||||||
|
url.searchParams.append('limit', limit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (search && typeof search === 'string') {
|
||||||
|
url.searchParams.append('search', search);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(url.toString(), {
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json, application/problem+json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
const apiResponse: ApiResponse<typeof data> = {
|
||||||
|
success: true,
|
||||||
|
data: data,
|
||||||
|
};
|
||||||
|
|
||||||
|
res.json(apiResponse);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching registry servers:', error);
|
||||||
|
const errorMessage =
|
||||||
|
error instanceof Error ? error.message : 'Failed to fetch registry servers';
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: errorMessage,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all versions of a specific MCP server
|
||||||
|
* Proxies the request to avoid CORS issues in the frontend
|
||||||
|
*/
|
||||||
|
export const getRegistryServerVersions = async (req: Request, res: Response): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const { serverName } = req.params;
|
||||||
|
|
||||||
|
if (!serverName) {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Server name is required',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// URL encode the server name
|
||||||
|
const encodedName = encodeURIComponent(serverName);
|
||||||
|
const response = await fetch(`${REGISTRY_BASE_URL}/servers/${encodedName}/versions`, {
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json, application/problem+json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status === 404) {
|
||||||
|
res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Server not found',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
const apiResponse: ApiResponse<typeof data> = {
|
||||||
|
success: true,
|
||||||
|
data: data,
|
||||||
|
};
|
||||||
|
|
||||||
|
res.json(apiResponse);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching registry server versions:', error);
|
||||||
|
const errorMessage =
|
||||||
|
error instanceof Error ? error.message : 'Failed to fetch registry server versions';
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: errorMessage,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a specific version of an MCP server
|
||||||
|
* Proxies the request to avoid CORS issues in the frontend
|
||||||
|
*/
|
||||||
|
export const getRegistryServerVersion = async (req: Request, res: Response): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const { serverName, version } = req.params;
|
||||||
|
|
||||||
|
if (!serverName || !version) {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Server name and version are required',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// URL encode the server name and version
|
||||||
|
const encodedName = encodeURIComponent(serverName);
|
||||||
|
const encodedVersion = encodeURIComponent(version);
|
||||||
|
const response = await fetch(
|
||||||
|
`${REGISTRY_BASE_URL}/servers/${encodedName}/versions/${encodedVersion}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json, application/problem+json',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status === 404) {
|
||||||
|
res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Server version not found',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
const apiResponse: ApiResponse<typeof data> = {
|
||||||
|
success: true,
|
||||||
|
data: data,
|
||||||
|
};
|
||||||
|
|
||||||
|
res.json(apiResponse);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching registry server version:', error);
|
||||||
|
const errorMessage =
|
||||||
|
error instanceof Error ? error.message : 'Failed to fetch registry server version';
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: errorMessage,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -56,9 +56,18 @@ import {
|
|||||||
getCloudServerToolsList,
|
getCloudServerToolsList,
|
||||||
callCloudTool,
|
callCloudTool,
|
||||||
} from '../controllers/cloudController.js';
|
} from '../controllers/cloudController.js';
|
||||||
|
import {
|
||||||
|
getAllRegistryServers,
|
||||||
|
getRegistryServerVersions,
|
||||||
|
getRegistryServerVersion,
|
||||||
|
} from '../controllers/registryController.js';
|
||||||
import { login, register, getCurrentUser, changePassword } from '../controllers/authController.js';
|
import { login, register, getCurrentUser, changePassword } from '../controllers/authController.js';
|
||||||
import { getAllLogs, clearLogs, streamLogs } from '../controllers/logController.js';
|
import { getAllLogs, clearLogs, streamLogs } from '../controllers/logController.js';
|
||||||
import { getRuntimeConfig, getPublicConfig, getMcpSettingsJson } from '../controllers/configController.js';
|
import {
|
||||||
|
getRuntimeConfig,
|
||||||
|
getPublicConfig,
|
||||||
|
getMcpSettingsJson,
|
||||||
|
} from '../controllers/configController.js';
|
||||||
import { callTool } from '../controllers/toolController.js';
|
import { callTool } from '../controllers/toolController.js';
|
||||||
import { getPrompt } from '../controllers/promptController.js';
|
import { getPrompt } from '../controllers/promptController.js';
|
||||||
import { uploadDxtFile, uploadMiddleware } from '../controllers/dxtController.js';
|
import { uploadDxtFile, uploadMiddleware } from '../controllers/dxtController.js';
|
||||||
@@ -144,6 +153,11 @@ export const initRoutes = (app: express.Application): void => {
|
|||||||
router.get('/cloud/servers/:serverName/tools', getCloudServerToolsList);
|
router.get('/cloud/servers/:serverName/tools', getCloudServerToolsList);
|
||||||
router.post('/cloud/servers/:serverName/tools/:toolName/call', callCloudTool);
|
router.post('/cloud/servers/:serverName/tools/:toolName/call', callCloudTool);
|
||||||
|
|
||||||
|
// Registry routes (proxy to official MCP registry)
|
||||||
|
router.get('/registry/servers', getAllRegistryServers);
|
||||||
|
router.get('/registry/servers/:serverName/versions', getRegistryServerVersions);
|
||||||
|
router.get('/registry/servers/:serverName/versions/:version', getRegistryServerVersion);
|
||||||
|
|
||||||
// Log routes
|
// Log routes
|
||||||
router.get('/logs', getAllLogs);
|
router.get('/logs', getAllLogs);
|
||||||
router.delete('/logs', clearLogs);
|
router.delete('/logs', clearLogs);
|
||||||
|
|||||||
Reference in New Issue
Block a user