Compare commits

...

16 Commits

Author SHA1 Message Date
samanhappy
26fa61fcfc feat: implement version checking and update notifications in AboutDialog and UserProfileMenu (#83) 2025-05-13 14:18:49 +08:00
samanhappy
d689541fc4 refactor: remove dependency on wait-for-npm job in build workflow (#82) 2025-05-13 13:07:21 +08:00
samanhappy
30895c4b9a refactor: remove wait-for-npm job from build workflow (#81) 2025-05-13 13:05:26 +08:00
samanhappy
37c3fd9e06 feat: add bearer authentication support for MCP requests (#79) 2025-05-13 13:02:41 +08:00
samanhappy
59454ca250 feat: add wait-for-npm job and update version from tag in build workflow (#80) 2025-05-13 13:01:54 +08:00
samanhappy
63efa0038c feat: add npmRegistry support to installation configuration (#77)
Co-authored-by: samanhappy@qq.com <my6051199>
2025-05-13 09:21:07 +08:00
samanhappy
040782da8d feat: support streamable http upstream server (#75)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-05-12 22:12:48 +08:00
samanhappy
f1a5f692cc feat: create MCP server for different session (#74) 2025-05-12 15:11:37 +08:00
samanhappy
5d798cfe6a feat: enhance release workflow to automatically update package.json version (#70) 2025-05-11 14:43:48 +08:00
samanhappy
0490d98c9e feat: add sponsorship section to README.md (#72) 2025-05-11 14:41:00 +08:00
samanhappy
7af3c8a2ba feat: add installation configuration support with pythonIndexUrl in settings (#67) 2025-05-10 21:33:35 +08:00
samanhappy
7c43ca359e feat: add acknowledgment section to README.zh.md (#68)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-05-10 21:33:22 +08:00
samanhappy
2bb6302cbc feat: enhance user experience with version info (#69)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-05-10 21:33:05 +08:00
samanhappy
3a3f6c984c Create FUNDING.yml (#65) 2025-05-10 11:05:07 +08:00
samanhappy
e8bc053788 fix: update react-router-dom to version 7.6.0 in package.json and pnpm-lock.yaml (#63) 2025-05-09 22:06:51 +08:00
samanhappy
bb674236c7 feat: update Node.js and pnpm versions in CI workflow; add packageManager field in package.json (#62) 2025-05-09 21:34:25 +08:00
30 changed files with 1079 additions and 236 deletions

15
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,15 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: samanhappy
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
thanks_dev: # Replace with a single thanks.dev username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

View File

@@ -16,6 +16,16 @@ jobs:
with:
fetch-depth: 0
- name: Update version from tag
if: startsWith(github.ref, 'refs/tags/')
run: |
VERSION=${GITHUB_REF#refs/tags/v}
echo "Updating package.json version to $VERSION"
jq ".version = \"$VERSION\"" package.json > package.json.tmp
mv package.json.tmp package.json
echo "Updated version in package.json:"
grep -m 1 "version" package.json
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
@@ -36,7 +46,7 @@ jobs:
type=raw,value=latest${{ matrix.variant == 'full' && '-full' || '' }},enable=${{ startsWith(github.ref, 'refs/tags/') }}
flavor: |
latest=false
- name: Build and Push Docker Image
uses: docker/build-push-action@v5
with:

View File

@@ -14,13 +14,13 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
node-version: '20'
registry-url: 'https://registry.npmjs.org'
- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: 8
version: 10
run_install: false
- name: Get pnpm store directory
@@ -42,13 +42,10 @@ jobs:
- name: Update version from tag
run: |
# 提取标签版本号(移除 'v' 前缀)
VERSION=${GITHUB_REF#refs/tags/v}
echo "Updating package.json version to $VERSION"
# 使用 jq 更新 package.json 中的版本号
jq ".version = \"$VERSION\"" package.json > package.json.tmp
mv package.json.tmp package.json
# 显示更新后的版本号
echo "Updated version in package.json:"
grep -m 1 "version" package.json

View File

@@ -13,6 +13,35 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Update package.json version
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Extract version from tag (remove 'v' prefix)
TAG_VERSION=${GITHUB_REF#refs/tags/v}
echo "Updating package.json version to $TAG_VERSION"
# Update package.json version field
sed -i "s/\"version\": \".*\"/\"version\": \"$TAG_VERSION\"/" package.json
# Commit the change
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git add package.json
git commit -m "chore: update version to $TAG_VERSION [skip ci]"
# Push using GITHUB_TOKEN for authentication
git remote set-url origin https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git
git push origin HEAD:${GITHUB_REF#refs/tags/}
- name: Release
uses: softprops/action-gh-release@v2
with:

View File

@@ -34,7 +34,6 @@ RUN if [ "$INSTALL_EXT" = "true" ]; then \
fi
RUN uv tool install mcp-server-fetch
ENV UV_PYTHON_INSTALL_MIRROR="http://mirrors.aliyun.com/pypi/simple/"
WORKDIR /app

View File

@@ -179,6 +179,12 @@ Contributions are welcome!
- Bug reports & fixes
- Translations & suggestions
## ❤️ Sponsor
If you like this project, maybe you can consider:
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/samanhappy)
## 📄 License
Licensed under the [Apache 2.0 License](LICENSE).

View File

@@ -203,6 +203,9 @@ proxy_buffering off
<img src="assets/reward.png" width="350">
## 致谢
感谢以下人员的赞赏:小白、琛。你们的支持是我继续前进的动力!
## 📄 许可证
本项目采用 [Apache 2.0 许可证](LICENSE)。

View File

@@ -18,6 +18,5 @@ if [ -n "$HTTPS_PROXY" ]; then
fi
echo "Using REQUEST_TIMEOUT: $REQUEST_TIMEOUT"
echo "Using UV_PYTHON_INSTALL_MIRROR: $UV_PYTHON_INSTALL_MIRROR"
exec "$@"

View File

@@ -12,9 +12,21 @@ interface ServerFormProps {
const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formError = null }: ServerFormProps) => {
const { t } = useTranslation()
const [serverType, setServerType] = useState<'sse' | 'stdio'>(
initialData && initialData.config && initialData.config.url ? 'sse' : 'stdio',
)
// Determine the initial server type from the initialData
const getInitialServerType = () => {
if (!initialData || !initialData.config) return 'stdio';
if (initialData.config.type) {
return initialData.config.type; // Use explicit type if available
} else if (initialData.config.url) {
return 'sse'; // Fallback to SSE if URL exists
} else {
return 'stdio'; // Default to stdio
}
};
const [serverType, setServerType] = useState<'stdio' | 'sse' | 'streamable-http'>(getInitialServerType());
const [formData, setFormData] = useState<ServerFormData>({
name: (initialData && initialData.name) || '',
@@ -27,6 +39,8 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
: String(initialData.config.args)
: '',
args: (initialData && initialData.config && initialData.config.args) || [],
type: getInitialServerType(), // Initialize the type field
env: []
})
const [envVars, setEnvVars] = useState<EnvVar[]>(
@@ -49,6 +63,11 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
setFormData({ ...formData, arguments: value, args })
}
const updateServerType = (type: 'stdio' | 'sse' | 'streamable-http') => {
setServerType(type);
setFormData(prev => ({ ...prev, type }));
}
const handleEnvVarChange = (index: number, field: 'key' | 'value', value: string) => {
const newEnvVars = [...envVars]
newEnvVars[index][field] = value
@@ -80,14 +99,17 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
const payload = {
name: formData.name,
config:
serverType === 'sse'
config: {
type: serverType, // Always include the type
...(serverType === 'sse' || serverType === 'streamable-http'
? { url: formData.url }
: {
command: formData.command,
args: formData.args,
env: Object.keys(env).length > 0 ? env : undefined,
},
command: formData.command,
args: formData.args,
env: Object.keys(env).length > 0 ? env : undefined,
}
)
}
}
onSubmit(payload)
@@ -139,10 +161,10 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
name="serverType"
value="command"
checked={serverType === 'stdio'}
onChange={() => setServerType('stdio')}
onChange={() => updateServerType('stdio')}
className="mr-1"
/>
<label htmlFor="command">stdio</label>
<label htmlFor="command">STDIO</label>
</div>
<div>
<input
@@ -151,15 +173,27 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
name="serverType"
value="url"
checked={serverType === 'sse'}
onChange={() => setServerType('sse')}
onChange={() => updateServerType('sse')}
className="mr-1"
/>
<label htmlFor="url">sse</label>
<label htmlFor="url">SSE</label>
</div>
<div>
<input
type="radio"
id="streamable-http"
name="serverType"
value="streamable-http"
checked={serverType === 'streamable-http'}
onChange={() => updateServerType('streamable-http')}
className="mr-1"
/>
<label htmlFor="streamable-http">Streamable HTTP</label>
</div>
</div>
</div>
{serverType === 'sse' ? (
{serverType === 'sse' || serverType === 'streamable-http' ? (
<div className="mb-4">
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="url">
{t('server.url')}
@@ -171,8 +205,8 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
value={formData.url}
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"
placeholder="e.g.: http://localhost:3000/sse"
required={serverType === 'sse'}
placeholder={serverType === 'streamable-http' ? "e.g.: http://localhost:3000/mcp" : "e.g.: http://localhost:3000/sse"}
required={serverType === 'sse' || serverType === 'streamable-http'}
/>
</div>
) : (

View File

@@ -1,6 +1,6 @@
import { ChevronDown, ChevronRight, Edit, Trash, Copy, Check } from 'lucide-react'
import { ChevronDown, ChevronRight, Edit, Trash, Copy, Check, User, Settings, LogOut, Info } from 'lucide-react'
export { ChevronDown, ChevronRight, Edit, Trash, Copy, Check }
export { ChevronDown, ChevronRight, Edit, Trash, Copy, Check, User, Settings, LogOut, Info }
const LucideIcons = {
ChevronDown,
@@ -8,7 +8,11 @@ const LucideIcons = {
Edit,
Trash,
Copy,
Check
Check,
User,
Settings,
LogOut,
Info
}
export default LucideIcons

View File

@@ -1,6 +1,5 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '@/contexts/AuthContext';
import ThemeSwitch from '@/components/ui/ThemeSwitch';
@@ -10,20 +9,14 @@ interface HeaderProps {
const Header: React.FC<HeaderProps> = ({ onToggleSidebar }) => {
const { t } = useTranslation();
const navigate = useNavigate();
const { auth, logout } = useAuth();
const handleLogout = () => {
logout();
navigate('/login');
};
const { auth } = useAuth();
return (
<header className="bg-white dark:bg-gray-800 shadow-sm z-10">
<div className="flex justify-between items-center px-4 py-3">
<div className="flex items-center">
{/* 侧边栏切换按钮 */}
<button
<button
onClick={onToggleSidebar}
className="p-2 rounded-md text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none"
aria-label={t('app.toggleSidebar')}
@@ -32,30 +25,15 @@ const Header: React.FC<HeaderProps> = ({ onToggleSidebar }) => {
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
{/* 应用标题 */}
<h1 className="ml-4 text-xl font-bold text-gray-900 dark:text-white">{t('app.title')}</h1>
</div>
{/* 用户信息和操作 */}
{/* Theme Switch */}
<div className="flex items-center space-x-4">
{/* Theme Switch */}
<ThemeSwitch />
{auth.user && (
<span className="text-sm text-gray-700 dark:text-gray-300">
{t('app.welcomeUser', { username: auth.user.username })}
</span>
)}
<div className="flex space-x-2">
<button
onClick={handleLogout}
className="px-3 py-1.5 bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-100 rounded hover:bg-red-200 dark:hover:bg-red-800 text-sm"
>
{t('app.logout')}
</button>
</div>
</div>
</div>
</header>

View File

@@ -1,6 +1,7 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { NavLink, useLocation } from 'react-router-dom';
import UserProfileMenu from '@/components/ui/UserProfileMenu';
interface SidebarProps {
collapsed: boolean;
@@ -16,6 +17,9 @@ const Sidebar: React.FC<SidebarProps> = ({ collapsed }) => {
const { t } = useTranslation();
const location = useLocation();
// Application version from package.json (accessed via Vite environment variables)
const appVersion = import.meta.env.PACKAGE_VERSION as string;
// Menu item configuration
const menuItems: MenuItem[] = [
{
@@ -64,42 +68,41 @@ const Sidebar: React.FC<SidebarProps> = ({ collapsed }) => {
</svg>
),
},
{
path: '/settings',
label: t('nav.settings'),
icon: (
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z" clipRule="evenodd" />
</svg>
),
},
];
return (
<aside
className={`bg-white dark:bg-gray-800 shadow-sm transition-all duration-300 ease-in-out ${
className={`bg-white dark:bg-gray-800 shadow-sm transition-all duration-300 ease-in-out flex flex-col h-full relative ${
collapsed ? 'w-16' : 'w-64'
}`}
>
<nav className="p-3 space-y-1">
{menuItems.map((item) => (
<NavLink
key={item.path}
to={item.path}
className={({ isActive }) =>
`flex items-center px-3 py-2 rounded-md transition-colors ${
isActive
? 'bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200'
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'
}`
}
end={item.path === '/'}
>
<span className="flex-shrink-0">{item.icon}</span>
{!collapsed && <span className="ml-3">{item.label}</span>}
</NavLink>
))}
</nav>
{/* Scrollable navigation area */}
<div className="overflow-y-auto flex-grow">
<nav className="p-3 space-y-1">
{menuItems.map((item) => (
<NavLink
key={item.path}
to={item.path}
className={({ isActive }) =>
`flex items-center px-3 py-2 rounded-md transition-colors ${
isActive
? 'bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200'
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'
}`
}
end={item.path === '/'}
>
<span className="flex-shrink-0">{item.icon}</span>
{!collapsed && <span className="ml-3">{item.label}</span>}
</NavLink>
))}
</nav>
</div>
{/* User profile menu fixed at the bottom */}
<div className="p-3 bg-white dark:bg-gray-800">
<UserProfileMenu collapsed={collapsed} version={appVersion} />
</div>
</aside>
);
};

View File

@@ -0,0 +1,112 @@
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { X, RefreshCw } from 'lucide-react';
import { checkLatestVersion, compareVersions } from '@/utils/version';
interface AboutDialogProps {
isOpen: boolean;
onClose: () => void;
version: string;
}
const AboutDialog: React.FC<AboutDialogProps> = ({ isOpen, onClose, version }) => {
const { t } = useTranslation();
const [hasNewVersion, setHasNewVersion] = useState(false);
const [latestVersion, setLatestVersion] = useState("");
const [isChecking, setIsChecking] = useState(false);
const checkForUpdates = async () => {
setIsChecking(true);
try {
const latest = await checkLatestVersion();
if (latest) {
setLatestVersion(latest);
setHasNewVersion(compareVersions(version, latest) > 0);
}
} catch (error) {
console.error('Failed to check for updates:', error);
} finally {
setIsChecking(false);
}
};
useEffect(() => {
if (isOpen) {
checkForUpdates();
}
}, [isOpen, version]);
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-black/50 bg-opacity-30 z-50 flex items-center justify-center p-4">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-lg max-w-md w-full">
<div className="p-6 relative">
{/* Close button (X) in the top-right corner */}
<button
onClick={onClose}
className="absolute top-4 right-4 text-gray-400 hover:text-gray-500 dark:text-gray-500 dark:hover:text-gray-400"
aria-label={t('common.close')}
>
<X className="h-5 w-5" />
</button>
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">
{t('about.title')}
</h3>
<div className="space-y-4">
<div className="flex items-center justify-between">
<span className="text-gray-700 dark:text-gray-300">
{t('about.currentVersion')}:
</span>
<span className="font-medium text-gray-900 dark:text-gray-100">
{version}
</span>
</div>
{hasNewVersion && latestVersion && (
<div className="bg-blue-50 dark:bg-blue-900 p-3 rounded">
<div className="flex items-start">
<div className="flex-shrink-0">
<svg className="h-5 w-5 text-blue-600 dark:text-blue-300" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div className="ml-3 flex-1 text-sm text-blue-700 dark:text-blue-300">
<p>{t('about.newVersionAvailable', { version: latestVersion })}</p>
<p className="mt-1">
<a
href="https://github.com/samanhappy/mcphub"
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 dark:text-blue-400 hover:underline"
>
{t('about.viewOnGitHub')}
</a>
</p>
</div>
</div>
</div>
)}
<button
onClick={checkForUpdates}
disabled={isChecking}
className={`mt-4 inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium
${isChecking
? 'text-gray-400 dark:text-gray-500 bg-gray-100 dark:bg-gray-800'
: 'text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600'
} focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500`}
>
<RefreshCw className={`h-4 w-4 mr-2 ${isChecking ? 'animate-spin' : ''}`} />
{isChecking ? t('about.checking') : t('about.checkForUpdates')}
</button>
</div>
</div>
</div>
</div>
);
};
export default AboutDialog;

View File

@@ -0,0 +1,132 @@
import React, { useState, useRef, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '@/contexts/AuthContext';
import { User, Settings, LogOut, Info } from 'lucide-react';
import AboutDialog from './AboutDialog';
import { checkLatestVersion, compareVersions } from '@/utils/version';
interface UserProfileMenuProps {
collapsed: boolean;
version: string;
}
const UserProfileMenu: React.FC<UserProfileMenuProps> = ({ collapsed, version }) => {
const { t } = useTranslation();
const navigate = useNavigate();
const { auth, logout } = useAuth();
const [isOpen, setIsOpen] = useState(false);
const [showNewVersionInfo, setShowNewVersionInfo] = useState(false);
const [showAboutDialog, setShowAboutDialog] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
// Check for new version on login and component mount
useEffect(() => {
const checkForNewVersion = async () => {
try {
const latestVersion = await checkLatestVersion();
if (latestVersion) {
setShowNewVersionInfo(compareVersions(version, latestVersion) > 0);
}
} catch (error) {
console.error('Error checking for new version:', error);
}
};
checkForNewVersion();
}, [version]);
// Close the menu when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
const handleSettingsClick = () => {
navigate('/settings');
setIsOpen(false);
};
const handleLogoutClick = () => {
logout();
navigate('/login');
};
const handleAboutClick = () => {
setShowAboutDialog(true);
setIsOpen(false);
};
return (
<div ref={menuRef} className="relative">
<button
onClick={() => setIsOpen(!isOpen)}
className={`flex ${collapsed ? 'justify-center' : 'items-center'} w-full p-2 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors rounded-md ${isOpen ? 'bg-gray-100 dark:bg-gray-700' : ''
}`}
>
<div className="flex-shrink-0 relative">
<div className="w-7 h-7 flex items-center justify-center rounded-full border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700">
<User className="h-4 w-4 text-gray-700 dark:text-gray-300" />
</div>
{showNewVersionInfo && (
<span className="absolute -top-1 -right-1 block w-2 h-2 bg-red-500 rounded-full"></span>
)}
</div>
{!collapsed && (
<div className="ml-3 flex flex-col items-start">
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
{auth.user?.username || t('auth.user')}
</span>
</div>
)}
</button>
{isOpen && (
<div className="absolute top-0 transform -translate-y-full left-0 w-48 bg-white dark:bg-gray-800 shadow-lg rounded-md py-1 z-50">
<button
onClick={handleSettingsClick}
className="flex items-center w-full px-4 py-2 text-left text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"
>
<Settings className="h-4 w-4 mr-2" />
{t('nav.settings')}
</button>
<button
onClick={handleAboutClick}
className="flex items-center w-full px-4 py-2 text-left text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 relative"
>
<Info className="h-4 w-4 mr-2" />
{t('about.title')}
&nbsp;({version})
{showNewVersionInfo && (
<span className="absolute top-2 right-4 block w-2 h-2 bg-red-500 rounded-full"></span>
)}
</button>
<button
onClick={handleLogoutClick}
className="flex items-center w-full px-4 py-2 text-left text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"
>
<LogOut className="h-4 w-4 mr-2" />
{t('app.logout')}
</button>
</div>
)}
{/* About dialog */}
<AboutDialog
isOpen={showAboutDialog}
onClose={() => setShowAboutDialog(false)}
version={version}
/>
</div>
);
};
export default UserProfileMenu;

View File

@@ -7,14 +7,26 @@ import { useToast } from '@/contexts/ToastContext';
interface RoutingConfig {
enableGlobalRoute: boolean;
enableGroupNameRoute: boolean;
enableBearerAuth: boolean;
bearerAuthKey: string;
}
interface InstallConfig {
pythonIndexUrl: string;
npmRegistry: string;
}
interface SystemSettings {
systemConfig?: {
routing?: RoutingConfig;
install?: InstallConfig;
};
}
interface TempRoutingConfig {
bearerAuthKey: string;
}
export const useSettingsData = () => {
const { t } = useTranslation();
const { showToast } = useToast();
@@ -22,6 +34,17 @@ export const useSettingsData = () => {
const [routingConfig, setRoutingConfig] = useState<RoutingConfig>({
enableGlobalRoute: true,
enableGroupNameRoute: true,
enableBearerAuth: false,
bearerAuthKey: '',
});
const [tempRoutingConfig, setTempRoutingConfig] = useState<TempRoutingConfig>({
bearerAuthKey: '',
});
const [installConfig, setInstallConfig] = useState<InstallConfig>({
pythonIndexUrl: '',
npmRegistry: '',
});
const [loading, setLoading] = useState(false);
@@ -56,6 +79,14 @@ export const useSettingsData = () => {
setRoutingConfig({
enableGlobalRoute: data.data.systemConfig.routing.enableGlobalRoute ?? true,
enableGroupNameRoute: data.data.systemConfig.routing.enableGroupNameRoute ?? true,
enableBearerAuth: data.data.systemConfig.routing.enableBearerAuth ?? false,
bearerAuthKey: data.data.systemConfig.routing.bearerAuthKey || '',
});
}
if (data.success && data.data?.systemConfig?.install) {
setInstallConfig({
pythonIndexUrl: data.data.systemConfig.install.pythonIndexUrl || '',
npmRegistry: data.data.systemConfig.install.npmRegistry || '',
});
}
} catch (error) {
@@ -68,7 +99,10 @@ export const useSettingsData = () => {
}, [t, showToast]);
// Update routing configuration
const updateRoutingConfig = async (key: keyof RoutingConfig, value: boolean) => {
const updateRoutingConfig = async <T extends keyof RoutingConfig>(
key: T,
value: RoutingConfig[T],
) => {
setLoading(true);
setError(null);
@@ -100,6 +134,53 @@ export const useSettingsData = () => {
});
showToast(t('settings.systemConfigUpdated'));
return true;
} else {
showToast(t('errors.failedToUpdateRouteConfig'));
return false;
}
} catch (error) {
console.error('Failed to update routing config:', error);
setError(error instanceof Error ? error.message : 'Failed to update routing config');
showToast(t('errors.failedToUpdateRouteConfig'));
return false;
} finally {
setLoading(false);
}
};
// Update install configuration
const updateInstallConfig = async (key: keyof InstallConfig, value: string) => {
setLoading(true);
setError(null);
try {
const token = localStorage.getItem('mcphub_token');
const response = await fetch('/api/system-config', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'x-auth-token': token || '',
},
body: JSON.stringify({
install: {
[key]: value,
},
}),
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const data = await response.json();
if (data.success) {
setInstallConfig({
...installConfig,
[key]: value,
});
showToast(t('settings.systemConfigUpdated'));
return true;
} else {
showToast(t('errors.failedToUpdateSystemConfig'));
return false;
@@ -119,13 +200,25 @@ export const useSettingsData = () => {
fetchSettings();
}, [fetchSettings, refreshKey]);
useEffect(() => {
if (routingConfig) {
setTempRoutingConfig({
bearerAuthKey: routingConfig.bearerAuthKey,
});
}
}, [routingConfig]);
return {
routingConfig,
tempRoutingConfig,
setTempRoutingConfig,
installConfig,
loading,
error,
setError,
triggerRefresh,
fetchSettings,
updateRoutingConfig,
updateInstallConfig,
};
};

View File

@@ -13,7 +13,7 @@ const MainLayout: React.FC = () => {
};
return (
<div className="flex flex-col min-h-screen bg-gray-100 dark:bg-gray-900">
<div className="flex flex-col h-screen bg-gray-100 dark:bg-gray-900">
{/* 顶部导航 */}
<Header onToggleSidebar={toggleSidebar} />

View File

@@ -12,6 +12,20 @@
"welcomeUser": "Welcome, {{username}}",
"name": "MCP Hub"
},
"about": {
"title": "About",
"versionInfo": "MCP Hub Version: {{version}}",
"newVersion": "New version available!",
"currentVersion": "Current version",
"newVersionAvailable": "New version {{version}} is available",
"viewOnGitHub": "View on GitHub",
"checkForUpdates": "Check for Updates",
"checking": "Checking for updates..."
},
"profile": {
"viewProfile": "View profile",
"userCenter": "User Center"
},
"theme": {
"title": "Theme",
"light": "Light",
@@ -105,7 +119,8 @@
"delete": "Delete",
"copy": "Copy",
"copySuccess": "Copied to clipboard",
"copyFailed": "Copy failed"
"copyFailed": "Copy failed",
"close": "Close"
},
"nav": {
"dashboard": "Dashboard",
@@ -137,7 +152,8 @@
"account": "Account Settings",
"password": "Change Password",
"appearance": "Appearance",
"routeConfig": "Route Configuration"
"routeConfig": "Security Configuration",
"installConfig": "Installation Configuration"
},
"market": {
"title": "Server Market - (Data from mcpm.sh)"
@@ -232,6 +248,18 @@
"enableGlobalRouteDescription": "Allow connections to /sse endpoint without specifying a group ID",
"enableGroupNameRoute": "Enable Group Name Route",
"enableGroupNameRouteDescription": "Allow connections to /sse endpoint using group names instead of just group IDs",
"enableBearerAuth": "Enable Bearer Authentication",
"enableBearerAuthDescription": "Require bearer token authentication for MCP requests",
"bearerAuthKey": "Bearer Authentication Key",
"bearerAuthKeyDescription": "The authentication key that will be required in the Bearer token",
"bearerAuthKeyPlaceholder": "Enter bearer authentication key",
"pythonIndexUrl": "Python Package Repository URL",
"pythonIndexUrlDescription": "Set UV_DEFAULT_INDEX environment variable for Python package installation",
"pythonIndexUrlPlaceholder": "e.g. https://pypi.org/simple",
"npmRegistry": "NPM Registry URL",
"npmRegistryDescription": "Set npm_config_registry environment variable for NPM package installation",
"npmRegistryPlaceholder": "e.g. https://registry.npmjs.org/",
"installConfig": "Installation Configuration",
"systemConfigUpdated": "System configuration updated successfully"
}
}

View File

@@ -12,6 +12,20 @@
"welcomeUser": "欢迎, {{username}}",
"name": "MCP Hub"
},
"about": {
"title": "关于",
"versionInfo": "MCP Hub 版本: {{version}}",
"newVersion": "有新版本可用!",
"currentVersion": "当前版本",
"newVersionAvailable": "新版本 {{version}} 已发布",
"viewOnGitHub": "在 GitHub 上查看",
"checkForUpdates": "检查更新",
"checking": "检查更新中..."
},
"profile": {
"viewProfile": "查看个人中心",
"userCenter": "个人中心"
},
"theme": {
"title": "主题",
"light": "浅色",
@@ -106,7 +120,8 @@
"delete": "删除",
"copy": "复制",
"copySuccess": "已复制到剪贴板",
"copyFailed": "复制失败"
"copyFailed": "复制失败",
"close": "关闭"
},
"nav": {
"dashboard": "仪表盘",
@@ -135,7 +150,8 @@
"account": "账户设置",
"password": "修改密码",
"appearance": "外观",
"routeConfig": "路由配置"
"routeConfig": "安全配置",
"installConfig": "安装配置"
},
"groups": {
"title": "分组管理"
@@ -230,9 +246,21 @@
},
"settings": {
"enableGlobalRoute": "启用全局路由",
"enableGlobalRouteDescription": "允许不指定组 ID 就连接到 /sse 端点",
"enableGroupNameRoute": "启用组名路由",
"enableGroupNameRouteDescription": "允许使用组名称而非分组 ID 连接到 /sse 端点",
"enableGlobalRouteDescription": "允许不指定组 ID 就连接到 /sse 端点",
"enableGroupNameRoute": "启用组名路由",
"enableGroupNameRouteDescription": "允许使用组名而不仅仅是组 ID 连接到 /sse 端点",
"enableBearerAuth": "启用 Bearer 认证",
"enableBearerAuthDescription": "对 MCP 请求启用 Bearer 令牌认证",
"bearerAuthKey": "Bearer 认证密钥",
"bearerAuthKeyDescription": "Bearer 令牌中需要携带的认证密钥",
"bearerAuthKeyPlaceholder": "请输入 Bearer 认证密钥",
"pythonIndexUrl": "Python 包仓库地址",
"pythonIndexUrlDescription": "设置 UV_DEFAULT_INDEX 环境变量,用于 Python 包安装",
"pythonIndexUrlPlaceholder": "例如: https://mirrors.aliyun.com/pypi/simple",
"npmRegistry": "NPM 仓库地址",
"npmRegistryDescription": "设置 npm_config_registry 环境变量,用于 NPM 包安装",
"npmRegistryPlaceholder": "例如: https://registry.npmmirror.com/",
"installConfig": "安装配置",
"systemConfigUpdated": "系统配置更新成功"
}
}

View File

@@ -5,6 +5,7 @@ import ChangePasswordForm from '@/components/ChangePasswordForm';
import { Switch } from '@/components/ui/ToggleGroup';
import { useSettingsData } from '@/hooks/useSettingsData';
import { useToast } from '@/contexts/ToastContext';
import { generateRandomKey } from '@/utils/key';
const SettingsPage: React.FC = () => {
const { t, i18n } = useTranslation();
@@ -17,26 +18,77 @@ const SettingsPage: React.FC = () => {
setCurrentLanguage(i18n.language);
}, [i18n.language]);
const [installConfig, setInstallConfig] = useState<{
pythonIndexUrl: string;
npmRegistry: string;
}>({
pythonIndexUrl: '',
npmRegistry: '',
});
const {
routingConfig,
tempRoutingConfig,
setTempRoutingConfig,
installConfig: savedInstallConfig,
loading,
updateRoutingConfig
updateRoutingConfig,
updateInstallConfig
} = useSettingsData();
// Update local installConfig when savedInstallConfig changes
useEffect(() => {
if (savedInstallConfig) {
setInstallConfig(savedInstallConfig);
}
}, [savedInstallConfig]);
const [sectionsVisible, setSectionsVisible] = useState({
routingConfig: false,
installConfig: false,
password: false
});
const toggleSection = (section: 'routingConfig' | 'password') => {
const toggleSection = (section: 'routingConfig' | 'installConfig' | 'password') => {
setSectionsVisible(prev => ({
...prev,
[section]: !prev[section]
}));
};
const handleRoutingConfigChange = async (key: 'enableGlobalRoute' | 'enableGroupNameRoute', value: boolean) => {
const handleRoutingConfigChange = async (key: 'enableGlobalRoute' | 'enableGroupNameRoute' | 'enableBearerAuth' | 'bearerAuthKey', value: boolean | string) => {
await updateRoutingConfig(key, value);
// If enableBearerAuth is turned on and there's no key, generate one
if (key === 'enableBearerAuth' && value === true) {
if (!tempRoutingConfig.bearerAuthKey) {
const newKey = generateRandomKey();
handleBearerAuthKeyChange(newKey);
await updateRoutingConfig('bearerAuthKey', newKey);
}
}
};
const handleBearerAuthKeyChange = (value: string) => {
setTempRoutingConfig(prev => ({
...prev,
bearerAuthKey: value
}));
};
const saveBearerAuthKey = async () => {
await updateRoutingConfig('bearerAuthKey', tempRoutingConfig.bearerAuthKey);
};
const handleInstallConfigChange = (key: 'pythonIndexUrl' | 'npmRegistry', value: string) => {
setInstallConfig({
...installConfig,
[key]: value
});
};
const saveInstallConfig = async (key: 'pythonIndexUrl' | 'npmRegistry') => {
await updateInstallConfig(key, installConfig[key]);
};
const handlePasswordChangeSuccess = () => {
@@ -60,21 +112,19 @@ const SettingsPage: React.FC = () => {
<h2 className="font-semibold text-gray-800">{t('pages.settings.language')}</h2>
<div className="flex space-x-3">
<button
className={`px-3 py-1.5 rounded-md transition-colors text-sm ${
currentLanguage.startsWith('en')
? 'bg-blue-500 text-white'
: 'bg-blue-100 text-blue-800 hover:bg-blue-200'
}`}
className={`px-3 py-1.5 rounded-md transition-colors text-sm ${currentLanguage.startsWith('en')
? 'bg-blue-500 text-white'
: 'bg-blue-100 text-blue-800 hover:bg-blue-200'
}`}
onClick={() => handleLanguageChange('en')}
>
English
</button>
<button
className={`px-3 py-1.5 rounded-md transition-colors text-sm ${
currentLanguage.startsWith('zh')
? 'bg-blue-500 text-white'
: 'bg-blue-100 text-blue-800 hover:bg-blue-200'
}`}
className={`px-3 py-1.5 rounded-md transition-colors text-sm ${currentLanguage.startsWith('zh')
? 'bg-blue-500 text-white'
: 'bg-blue-100 text-blue-800 hover:bg-blue-200'
}`}
onClick={() => handleLanguageChange('zh')}
>
@@ -97,6 +147,44 @@ const SettingsPage: React.FC = () => {
{sectionsVisible.routingConfig && (
<div className="space-y-4 mt-4">
<div className="flex items-center justify-between p-3 bg-gray-50 rounded-md">
<div>
<h3 className="font-medium text-gray-700">{t('settings.enableBearerAuth')}</h3>
<p className="text-sm text-gray-500">{t('settings.enableBearerAuthDescription')}</p>
</div>
<Switch
disabled={loading}
checked={routingConfig.enableBearerAuth}
onCheckedChange={(checked) => handleRoutingConfigChange('enableBearerAuth', checked)}
/>
</div>
{routingConfig.enableBearerAuth && (
<div className="p-3 bg-gray-50 rounded-md">
<div className="mb-2">
<h3 className="font-medium text-gray-700">{t('settings.bearerAuthKey')}</h3>
<p className="text-sm text-gray-500">{t('settings.bearerAuthKeyDescription')}</p>
</div>
<div className="flex items-center gap-3">
<input
type="text"
value={tempRoutingConfig.bearerAuthKey}
onChange={(e) => handleBearerAuthKeyChange(e.target.value)}
placeholder={t('settings.bearerAuthKeyPlaceholder')}
className="flex-1 mt-1 block w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
disabled={loading || !routingConfig.enableBearerAuth}
/>
<button
onClick={saveBearerAuthKey}
disabled={loading || !routingConfig.enableBearerAuth}
className="mt-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md text-sm font-medium disabled:opacity-50"
>
{t('common.save')}
</button>
</div>
</div>
)}
<div className="flex items-center justify-between p-3 bg-gray-50 rounded-md">
<div>
<h3 className="font-medium text-gray-700">{t('settings.enableGlobalRoute')}</h3>
@@ -120,6 +208,72 @@ const SettingsPage: React.FC = () => {
onCheckedChange={(checked) => handleRoutingConfigChange('enableGroupNameRoute', checked)}
/>
</div>
</div>
)}
</div>
{/* Installation Configuration Settings */}
<div className="bg-white shadow rounded-lg py-4 px-6 mb-6">
<div
className="flex justify-between items-center cursor-pointer"
onClick={() => toggleSection('installConfig')}
>
<h2 className="font-semibold text-gray-800">{t('settings.installConfig')}</h2>
<span className="text-gray-500">
{sectionsVisible.installConfig ? '▼' : '►'}
</span>
</div>
{sectionsVisible.installConfig && (
<div className="space-y-4 mt-4">
<div className="p-3 bg-gray-50 rounded-md">
<div className="mb-2">
<h3 className="font-medium text-gray-700">{t('settings.pythonIndexUrl')}</h3>
<p className="text-sm text-gray-500">{t('settings.pythonIndexUrlDescription')}</p>
</div>
<div className="flex items-center gap-3">
<input
type="text"
value={installConfig.pythonIndexUrl}
onChange={(e) => handleInstallConfigChange('pythonIndexUrl', e.target.value)}
placeholder={t('settings.pythonIndexUrlPlaceholder')}
className="flex-1 mt-1 block w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
disabled={loading}
/>
<button
onClick={() => saveInstallConfig('pythonIndexUrl')}
disabled={loading}
className="mt-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md text-sm font-medium disabled:opacity-50"
>
{t('common.save')}
</button>
</div>
</div>
<div className="p-3 bg-gray-50 rounded-md">
<div className="mb-2">
<h3 className="font-medium text-gray-700">{t('settings.npmRegistry')}</h3>
<p className="text-sm text-gray-500">{t('settings.npmRegistryDescription')}</p>
</div>
<div className="flex items-center gap-3">
<input
type="text"
value={installConfig.npmRegistry}
onChange={(e) => handleInstallConfigChange('npmRegistry', e.target.value)}
placeholder={t('settings.npmRegistryPlaceholder')}
className="flex-1 mt-1 block w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
disabled={loading}
/>
<button
onClick={() => saveInstallConfig('npmRegistry')}
disabled={loading}
className="mt-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md text-sm font-medium disabled:opacity-50"
>
{t('common.save')}
</button>
</div>
</div>
</div>
)}
</div>

View File

@@ -71,6 +71,7 @@ export interface Tool {
// Server config types
export interface ServerConfig {
type?: 'stdio' | 'sse' | 'streamable-http';
url?: string;
command?: string;
args?: string[];
@@ -108,6 +109,8 @@ export interface ServerFormData {
url: string;
command: string;
arguments: string;
args?: string[]; // Added explicit args field
type?: 'stdio' | 'sse' | 'streamable-http'; // Added type field
env: EnvVar[];
}
@@ -157,4 +160,4 @@ export interface AuthResponse {
token?: string;
user?: IUser;
message?: string;
}
}

View File

@@ -0,0 +1,8 @@
export function generateRandomKey(length: number = 32): string {
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
const array = new Uint8Array(length);
crypto.getRandomValues(array);
return Array.from(array)
.map((x) => characters.charAt(x % characters.length))
.join('');
}

View File

@@ -0,0 +1,31 @@
const NPM_REGISTRY = 'https://registry.npmjs.org';
const PACKAGE_NAME = '@samanhappy/mcphub';
export const checkLatestVersion = async (): Promise<string | null> => {
try {
const response = await fetch(`${NPM_REGISTRY}/${PACKAGE_NAME}/latest`);
if (!response.ok) {
throw new Error(`Failed to fetch latest version: ${response.status}`);
}
const data = await response.json();
return data.version || null;
} catch (error) {
console.error('Error checking for latest version:', error);
return null;
}
};
export const compareVersions = (current: string, latest: string): number => {
const currentParts = current.split('.').map(Number);
const latestParts = latest.split('.').map(Number);
for (let i = 0; i < 3; i++) {
const currentPart = currentParts[i] || 0;
const latestPart = latestParts[i] || 0;
if (currentPart > latestPart) return -1;
if (currentPart < latestPart) return 1;
}
return 0;
};

9
frontend/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1,9 @@
/// <reference types="vite/client" />
interface ImportMeta {
readonly env: {
readonly PACKAGE_VERSION: string;
// Add other custom env variables here if needed
[key: string]: any;
};
}

View File

@@ -2,6 +2,11 @@ import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
import tailwindcss from '@tailwindcss/vite';
// Import the package.json to get the version
import { readFileSync } from 'fs';
// Get package.json version
const packageJson = JSON.parse(readFileSync(path.resolve(__dirname, '../package.json'), 'utf-8'));
// https://vitejs.dev/config/
export default defineConfig({
@@ -11,6 +16,13 @@ export default defineConfig({
'@': path.resolve(__dirname, './src'),
},
},
define: {
// Make package version available as global variable
'import.meta.env.PACKAGE_VERSION': JSON.stringify(packageJson.version),
},
build: {
sourcemap: true, // Enable source maps for production build
},
server: {
proxy: {
'/api': {

View File

@@ -1,6 +1,6 @@
{
"name": "@samanhappy/mcphub",
"version": "0.0.27",
"version": "0.5.4",
"description": "A hub server for mcp servers",
"main": "dist/index.js",
"type": "module",
@@ -21,6 +21,7 @@
"backend:build": "tsc",
"start": "node dist/index.js",
"backend:dev": "tsx watch src/index.ts",
"backend:debug": "tsx watch src/index.ts --inspect",
"lint": "eslint . --ext .ts",
"format": "prettier --write \"src/**/*.ts\"",
"test": "jest",
@@ -28,6 +29,7 @@
"frontend:build": "cd frontend && vite build",
"frontend:preview": "cd frontend && vite preview",
"dev": "concurrently \"pnpm backend:dev\" \"pnpm frontend:dev\"",
"debug": "concurrently \"pnpm backend:debug\" \"pnpm frontend:dev\"",
"prepublishOnly": "npm run build && node scripts/verify-dist.js"
},
"keywords": [
@@ -39,7 +41,7 @@
"author": "",
"license": "ISC",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.10.2",
"@modelcontextprotocol/sdk": "^1.11.1",
"bcryptjs": "^3.0.2",
"dotenv": "^16.3.1",
"express": "^4.18.2",
@@ -79,7 +81,7 @@
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-i18next": "^15.4.1",
"react-router-dom": "^7.5.0",
"react-router-dom": "^7.6.0",
"tailwind-merge": "^3.1.0",
"tailwind-scrollbar-hide": "^2.0.0",
"tailwindcss": "^4.0.17",
@@ -92,5 +94,6 @@
},
"engines": {
"node": ">=16.0.0"
}
},
"packageManager": "pnpm@10.10.0+sha256.fa0f513aa8191764d2b6b432420788c270f07b4f999099b65bb2010eec702a30"
}

42
pnpm-lock.yaml generated
View File

@@ -9,8 +9,8 @@ importers:
.:
dependencies:
'@modelcontextprotocol/sdk':
specifier: ^1.10.2
version: 1.10.2
specifier: ^1.11.1
version: 1.11.1
bcryptjs:
specifier: ^3.0.2
version: 3.0.2
@@ -124,8 +124,8 @@ importers:
specifier: ^15.4.1
version: 15.4.1(i18next@24.2.3(typescript@5.8.2))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
react-router-dom:
specifier: ^7.5.0
version: 7.5.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
specifier: ^7.6.0
version: 7.6.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
tailwind-merge:
specifier: ^3.1.0
version: 3.1.0
@@ -867,8 +867,8 @@ packages:
'@jridgewell/trace-mapping@0.3.9':
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
'@modelcontextprotocol/sdk@1.10.2':
resolution: {integrity: sha512-rb6AMp2DR4SN+kc6L1ta2NCpApyA9WYNx3CrTSZvGxq9wH71bRur+zRqPfg0vQ9mjywR7qZdX2RGHOPq3ss+tA==}
'@modelcontextprotocol/sdk@1.11.1':
resolution: {integrity: sha512-9LfmxKTb1v+vUS1/emSk1f5ePmTLkb9Le9AxOB5T0XM59EUumwcS45z05h7aiZx3GI0Bl7mjb3FMEglYj+acuQ==}
engines: {node: '>=18'}
'@next/env@15.2.4':
@@ -1308,9 +1308,6 @@ packages:
'@types/connect@3.4.38':
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
'@types/cookie@0.6.0':
resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
'@types/estree@1.0.7':
resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==}
@@ -3032,15 +3029,15 @@ packages:
resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==}
engines: {node: '>=0.10.0'}
react-router-dom@7.5.0:
resolution: {integrity: sha512-fFhGFCULy4vIseTtH5PNcY/VvDJK5gvOWcwJVHQp8JQcWVr85ENhJ3UpuF/zP1tQOIFYNRJHzXtyhU1Bdgw0RA==}
react-router-dom@7.6.0:
resolution: {integrity: sha512-DYgm6RDEuKdopSyGOWZGtDfSm7Aofb8CCzgkliTjtu/eDuB0gcsv6qdFhhi8HdtmA+KHkt5MfZ5K2PdzjugYsA==}
engines: {node: '>=20.0.0'}
peerDependencies:
react: '>=18'
react-dom: '>=18'
react-router@7.5.0:
resolution: {integrity: sha512-estOHrRlDMKdlQa6Mj32gIks4J+AxNsYoE0DbTTxiMy2mPzZuWSDU+N85/r1IlNR7kGfznF3VCUlvc5IUO+B9g==}
react-router@7.6.0:
resolution: {integrity: sha512-GGufuHIVCJDbnIAXP3P9Sxzq3UUsddG3rrI3ut1q6m0FI6vxVBF3JoPQ38+W/blslLH4a5Yutp8drkEpXoddGQ==}
engines: {node: '>=20.0.0'}
peerDependencies:
react: '>=18'
@@ -3420,9 +3417,6 @@ packages:
engines: {node: '>=18.0.0'}
hasBin: true
turbo-stream@2.4.0:
resolution: {integrity: sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==}
type-check@0.4.0:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'}
@@ -4269,7 +4263,7 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.0
'@modelcontextprotocol/sdk@1.10.2':
'@modelcontextprotocol/sdk@1.11.1':
dependencies:
content-type: 1.0.5
cors: 2.8.5
@@ -4637,8 +4631,6 @@ snapshots:
dependencies:
'@types/node': 20.17.28
'@types/cookie@0.6.0': {}
'@types/estree@1.0.7': {}
'@types/express-serve-static-core@4.19.6':
@@ -6652,19 +6644,17 @@ snapshots:
react-refresh@0.14.2: {}
react-router-dom@7.5.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
react-router-dom@7.6.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
dependencies:
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
react-router: 7.5.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
react-router: 7.6.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
react-router@7.5.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
react-router@7.6.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
dependencies:
'@types/cookie': 0.6.0
cookie: 1.0.2
react: 19.1.0
set-cookie-parser: 2.7.1
turbo-stream: 2.4.0
optionalDependencies:
react-dom: 19.1.0(react@19.1.0)
@@ -6791,7 +6781,7 @@ snapshots:
send@1.2.0:
dependencies:
debug: 4.3.6
debug: 4.4.0
encodeurl: 2.0.0
escape-html: 1.0.3
etag: 1.8.1
@@ -7089,8 +7079,6 @@ snapshots:
optionalDependencies:
fsevents: 2.3.3
turbo-stream@2.4.0: {}
type-check@0.4.0:
dependencies:
prelude-ls: 1.2.1

View File

@@ -69,6 +69,24 @@ export const createServer = async (req: Request, res: Response): Promise<void> =
return;
}
// Validate the server type if specified
if (config.type && !['stdio', 'sse', 'streamable-http'].includes(config.type)) {
res.status(400).json({
success: false,
message: 'Server type must be one of: stdio, sse, streamable-http',
});
return;
}
// Validate that URL is provided for sse and streamable-http types
if ((config.type === 'sse' || config.type === 'streamable-http') && !config.url) {
res.status(400).json({
success: false,
message: `URL is required for ${config.type} server type`,
});
return;
}
const result = await addServer(name, config);
if (result.success) {
notifyToolChanged();
@@ -150,6 +168,24 @@ export const updateServer = async (req: Request, res: Response): Promise<void> =
return;
}
// Validate the server type if specified
if (config.type && !['stdio', 'sse', 'streamable-http'].includes(config.type)) {
res.status(400).json({
success: false,
message: 'Server type must be one of: stdio, sse, streamable-http',
});
return;
}
// Validate that URL is provided for sse and streamable-http types
if ((config.type === 'sse' || config.type === 'streamable-http') && !config.url) {
res.status(400).json({
success: false,
message: `URL is required for ${config.type} server type`,
});
return;
}
const result = await updateMcpServer(name, config);
if (result.success) {
notifyToolChanged();
@@ -247,41 +283,83 @@ export const toggleServer = async (req: Request, res: Response): Promise<void> =
export const updateSystemConfig = (req: Request, res: Response): void => {
try {
const { routing } = req.body;
if (!routing || (typeof routing.enableGlobalRoute !== 'boolean' && typeof routing.enableGroupNameRoute !== 'boolean')) {
const { routing, install } = req.body;
if (
(!routing ||
(typeof routing.enableGlobalRoute !== 'boolean' &&
typeof routing.enableGroupNameRoute !== 'boolean' &&
typeof routing.enableBearerAuth !== 'boolean' &&
typeof routing.bearerAuthKey !== 'string')) &&
(!install ||
(typeof install.pythonIndexUrl !== 'string' && typeof install.npmRegistry !== 'string'))
) {
res.status(400).json({
success: false,
message: 'Invalid system configuration provided',
});
return;
}
const settings = loadSettings();
if (!settings.systemConfig) {
settings.systemConfig = {
routing: {
enableGlobalRoute: true,
enableGroupNameRoute: true
}
enableGroupNameRoute: true,
enableBearerAuth: false,
bearerAuthKey: '',
},
install: {
pythonIndexUrl: '',
npmRegistry: '',
},
};
}
if (!settings.systemConfig.routing) {
settings.systemConfig.routing = {
enableGlobalRoute: true,
enableGroupNameRoute: true
enableGroupNameRoute: true,
enableBearerAuth: false,
bearerAuthKey: '',
};
}
if (typeof routing.enableGlobalRoute === 'boolean') {
settings.systemConfig.routing.enableGlobalRoute = routing.enableGlobalRoute;
if (!settings.systemConfig.install) {
settings.systemConfig.install = {
pythonIndexUrl: '',
npmRegistry: '',
};
}
if (typeof routing.enableGroupNameRoute === 'boolean') {
settings.systemConfig.routing.enableGroupNameRoute = routing.enableGroupNameRoute;
if (routing) {
if (typeof routing.enableGlobalRoute === 'boolean') {
settings.systemConfig.routing.enableGlobalRoute = routing.enableGlobalRoute;
}
if (typeof routing.enableGroupNameRoute === 'boolean') {
settings.systemConfig.routing.enableGroupNameRoute = routing.enableGroupNameRoute;
}
if (typeof routing.enableBearerAuth === 'boolean') {
settings.systemConfig.routing.enableBearerAuth = routing.enableBearerAuth;
}
if (typeof routing.bearerAuthKey === 'string') {
settings.systemConfig.routing.bearerAuthKey = routing.bearerAuthKey;
}
}
if (install) {
if (typeof install.pythonIndexUrl === 'string') {
settings.systemConfig.install.pythonIndexUrl = install.pythonIndexUrl;
}
if (typeof install.npmRegistry === 'string') {
settings.systemConfig.install.npmRegistry = install.npmRegistry;
}
}
if (saveSettings(settings)) {
res.json({
success: true,

View File

@@ -3,37 +3,43 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprot
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
import { ServerInfo, ServerConfig } from '../types/index.js';
import { loadSettings, saveSettings, expandEnvVars } from '../config/index.js';
import config from '../config/index.js';
import { getGroup } from './sseService.js';
import { getServersInGroup } from './groupService.js';
let currentServer: Server;
const servers: { [sessionId: string]: Server } = {};
export const initMcpServer = async (name: string, version: string): Promise<void> => {
currentServer = createMcpServer(name, version);
await registerAllTools(currentServer, true, true);
await registerAllTools(true);
};
export const setMcpServer = (server: Server): void => {
currentServer = server;
export const getMcpServer = (sessionId: string): Server => {
if (!servers[sessionId]) {
const server = createMcpServer(config.mcpHubName, config.mcpHubVersion);
servers[sessionId] = server;
}
return servers[sessionId];
};
export const getMcpServer = (): Server => {
return currentServer;
export const deleteMcpServer = (sessionId: string): void => {
delete servers[sessionId];
};
export const notifyToolChanged = async () => {
await registerAllTools(currentServer, true, false);
currentServer
.sendToolListChanged()
.catch((error) => {
console.warn('Failed to send tool list changed notification:', error.message);
})
.then(() => {
console.log('Tool list changed notification sent successfully');
});
await registerAllTools(false);
Object.values(servers).forEach((server) => {
server
.sendToolListChanged()
.catch((error) => {
console.warn('Failed to send tool list changed notification:', error.message);
})
.then(() => {
console.log('Tool list changed notification sent successfully');
});
});
};
// Store all server information
@@ -74,11 +80,30 @@ export const initializeClientsFromSettings = (isInit: boolean): ServerInfo[] =>
}
let transport;
if (conf.url) {
if (conf.type === 'streamable-http') {
transport = new StreamableHTTPClientTransport(new URL(conf.url || ''));
} else if (conf.url) {
// Default to SSE only when 'conf.type' is not specified and 'conf.url' is available
transport = new SSEClientTransport(new URL(conf.url));
} else if (conf.command && conf.args) {
// If type is stdio or if command and args are provided without type
const env: Record<string, string> = conf.env || {};
env['PATH'] = expandEnvVars(process.env.PATH as string) || '';
// Add UV_DEFAULT_INDEX from settings if available (for Python packages)
const settings = loadSettings(); // Add UV_DEFAULT_INDEX from settings if available (for Python packages)
if (settings.systemConfig?.install?.pythonIndexUrl && conf.command === 'uvx') {
env['UV_DEFAULT_INDEX'] = settings.systemConfig.install.pythonIndexUrl;
}
// Add npm_config_registry from settings if available (for NPM packages)
if (
settings.systemConfig?.install?.npmRegistry &&
(conf.command === 'npm' || conf.command === 'npx')
) {
env['npm_config_registry'] = settings.systemConfig.install.npmRegistry;
}
transport = new StdioClientTransport({
command: conf.command,
args: conf.args,
@@ -173,11 +198,7 @@ export const initializeClientsFromSettings = (isInit: boolean): ServerInfo[] =>
};
// Register all MCP tools
export const registerAllTools = async (
server: Server,
forceInit: boolean,
isInit: boolean,
): Promise<void> => {
export const registerAllTools = async (isInit: boolean): Promise<void> => {
initializeClientsFromSettings(isInit);
};
@@ -229,7 +250,7 @@ export const addServer = async (
return { success: false, message: 'Failed to save settings' };
}
registerAllTools(currentServer, false, false);
registerAllTools(false);
return { success: true, message: 'Server added successfully' };
} catch (error) {
console.error(`Failed to add server: ${name}`, error);
@@ -336,59 +357,62 @@ export const toggleServerStatus = async (
}
};
const handleListToolsRequest = async (_: any, extra: any) => {
const sessionId = extra.sessionId || '';
const group = getGroup(sessionId);
console.log(`Handling ListToolsRequest for group: ${group}`);
const allServerInfos = serverInfos.filter((serverInfo) => {
if (serverInfo.enabled === false) return false;
if (!group) return true;
const serversInGroup = getServersInGroup(group);
if (!serversInGroup || serversInGroup.length === 0) return serverInfo.name === group;
return serversInGroup.includes(serverInfo.name);
});
const allTools = [];
for (const serverInfo of allServerInfos) {
if (serverInfo.tools && serverInfo.tools.length > 0) {
allTools.push(...serverInfo.tools);
}
}
return {
tools: allTools,
};
};
const handleCallToolRequest = async (request: any, extra: any) => {
console.log(`Handling CallToolRequest for tool: ${request.params.name}`);
try {
const serverInfo = getServerByTool(request.params.name);
if (!serverInfo) {
throw new Error(`Server not found: ${request.params.name}`);
}
const client = serverInfo.client;
if (!client) {
throw new Error(`Client not found for server: ${request.params.name}`);
}
const result = await client.callTool(request.params);
console.log(`Tool call result: ${JSON.stringify(result)}`);
return result;
} catch (error) {
console.error(`Error handling CallToolRequest: ${error}`);
return {
content: [
{
type: 'text',
text: `Error: ${error}`,
},
],
isError: true,
};
}
};
// Create McpServer instance
export const createMcpServer = (name: string, version: string): Server => {
const server = new Server({ name, version }, { capabilities: { tools: {} } });
server.setRequestHandler(ListToolsRequestSchema, async (_, extra) => {
const sessionId = extra.sessionId || '';
const group = getGroup(sessionId);
console.log(`Handling ListToolsRequest for group: ${group}`);
const allServerInfos = serverInfos.filter((serverInfo) => {
if (serverInfo.enabled === false) return false;
if (!group) return true;
const serversInGroup = getServersInGroup(group);
if (!serversInGroup || serversInGroup.length === 0) return serverInfo.name === group;
return serversInGroup.includes(serverInfo.name);
});
const allTools = [];
for (const serverInfo of allServerInfos) {
if (serverInfo.tools && serverInfo.tools.length > 0) {
allTools.push(...serverInfo.tools);
}
}
return {
tools: allTools,
};
});
server.setRequestHandler(CallToolRequestSchema, async (request, _) => {
console.log(`Handling CallToolRequest for tool: ${request.params.name}`);
try {
const serverInfo = getServerByTool(request.params.name);
if (!serverInfo) {
throw new Error(`Server not found: ${request.params.name}`);
}
const client = serverInfo.client;
if (!client) {
throw new Error(`Client not found for server: ${request.params.name}`);
}
const result = await client.callTool(request.params);
console.log(`Tool call result: ${JSON.stringify(result)}`);
return result;
} catch (error) {
console.error(`Error handling CallToolRequest: ${error}`);
return {
content: [
{
type: 'text',
text: `Error: ${error}`,
},
],
isError: true,
};
}
});
server.setRequestHandler(ListToolsRequestSchema, handleListToolsRequest);
server.setRequestHandler(CallToolRequestSchema, handleCallToolRequest);
return server;
};

View File

@@ -4,7 +4,7 @@ import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
import { getMcpServer } from './mcpService.js';
import { deleteMcpServer, getMcpServer } from './mcpService.js';
import { loadSettings } from '../config/index.js';
const transports: { [sessionId: string]: { transport: Transport; group: string } } = {};
@@ -13,11 +13,42 @@ export const getGroup = (sessionId: string): string => {
return transports[sessionId]?.group || '';
};
export const handleSseConnection = async (req: Request, res: Response): Promise<void> => {
// Helper function to validate bearer auth
const validateBearerAuth = (req: Request): boolean => {
const settings = loadSettings();
const routingConfig = settings.systemConfig?.routing || {
enableGlobalRoute: true,
enableGroupNameRoute: true,
enableBearerAuth: false,
bearerAuthKey: '',
};
if (routingConfig.enableBearerAuth) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return false;
}
const token = authHeader.substring(7); // Remove "Bearer " prefix
return token === routingConfig.bearerAuthKey;
}
return true;
};
export const handleSseConnection = async (req: Request, res: Response): Promise<void> => {
// Check bearer auth
if (!validateBearerAuth(req)) {
res.status(401).send('Bearer authentication required or invalid token');
return;
}
const settings = loadSettings();
const routingConfig = settings.systemConfig?.routing || {
enableGlobalRoute: true,
enableGroupNameRoute: true,
enableBearerAuth: false,
bearerAuthKey: '',
};
const group = req.params.group;
@@ -32,16 +63,23 @@ export const handleSseConnection = async (req: Request, res: Response): Promise<
res.on('close', () => {
delete transports[transport.sessionId];
deleteMcpServer(transport.sessionId);
console.log(`SSE connection closed: ${transport.sessionId}`);
});
console.log(
`New SSE connection established: ${transport.sessionId} with group: ${group || 'global'}`,
);
await getMcpServer().connect(transport);
await getMcpServer(transport.sessionId).connect(transport);
};
export const handleSseMessage = async (req: Request, res: Response): Promise<void> => {
// Check bearer auth
if (!validateBearerAuth(req)) {
res.status(401).send('Bearer authentication required or invalid token');
return;
}
const sessionId = req.query.sessionId as string;
const { transport, group } = transports[sessionId];
req.params.group = group;
@@ -56,9 +94,15 @@ export const handleSseMessage = async (req: Request, res: Response): Promise<voi
};
export const handleMcpPostRequest = async (req: Request, res: Response): Promise<void> => {
console.log('Handling MCP post request');
const sessionId = req.headers['mcp-session-id'] as string | undefined;
const group = req.params.group;
console.log(`Handling MCP post request for sessionId: ${sessionId} and group: ${group}`);
// Check bearer auth
if (!validateBearerAuth(req)) {
res.status(401).send('Bearer authentication required or invalid token');
return;
}
const settings = loadSettings();
const routingConfig = settings.systemConfig?.routing || {
enableGlobalRoute: true,
@@ -71,6 +115,7 @@ export const handleMcpPostRequest = async (req: Request, res: Response): Promise
let transport: StreamableHTTPServerTransport;
if (sessionId && transports[sessionId]) {
console.log(`Reusing existing transport for sessionId: ${sessionId}`);
transport = transports[sessionId].transport as StreamableHTTPServerTransport;
} else if (!sessionId && isInitializeRequest(req.body)) {
transport = new StreamableHTTPServerTransport({
@@ -83,10 +128,13 @@ export const handleMcpPostRequest = async (req: Request, res: Response): Promise
transport.onclose = () => {
if (transport.sessionId) {
delete transports[transport.sessionId];
deleteMcpServer(transport.sessionId);
console.log(`MCP connection closed: ${transport.sessionId}`);
}
};
await getMcpServer().connect(transport);
console.log(`MCP connection established: ${transport.sessionId}`);
await getMcpServer(transport.sessionId || 'mcp').connect(transport);
} else {
res.status(400).json({
jsonrpc: '2.0',
@@ -99,11 +147,18 @@ export const handleMcpPostRequest = async (req: Request, res: Response): Promise
return;
}
console.log(`Handling request using transport with type ${transport.constructor.name}`);
await transport.handleRequest(req, res, req.body);
};
export const handleMcpOtherRequest = async (req: Request, res: Response) => {
console.log('Handling MCP other request');
// Check bearer auth
if (!validateBearerAuth(req)) {
res.status(401).send('Bearer authentication required or invalid token');
return;
}
const sessionId = req.headers['mcp-session-id'] as string | undefined;
if (!sessionId || !transports[sessionId]) {
res.status(400).send('Invalid or missing session ID');

View File

@@ -1,6 +1,7 @@
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
// User interface
export interface IUser {
@@ -11,8 +12,8 @@ export interface IUser {
// Group interface for server grouping
export interface IGroup {
id: string; // Unique UUID for the group
name: string; // Display name of the group
id: string; // Unique UUID for the group
name: string; // Display name of the group
description?: string; // Optional description of the group
servers: string[]; // Array of server names that belong to this group
}
@@ -82,6 +83,12 @@ export interface McpSettings {
routing?: {
enableGlobalRoute?: boolean; // Controls whether the /sse endpoint without group is enabled
enableGroupNameRoute?: boolean; // Controls whether group routing by name is allowed
enableBearerAuth?: boolean; // Controls whether bearer auth is enabled for group routes
bearerAuthKey?: string; // The bearer auth key to validate against
};
install?: {
pythonIndexUrl?: string; // Python package repository URL (UV_DEFAULT_INDEX)
npmRegistry?: string; // NPM registry URL (npm_config_registry)
};
// Add other system configuration sections here in the future
};
@@ -89,7 +96,8 @@ export interface McpSettings {
// Configuration details for an individual server
export interface ServerConfig {
url?: string; // URL for SSE-based servers
type?: 'stdio' | 'sse' | 'streamable-http'; // Type of server
url?: string; // URL for SSE or streamable HTTP servers
command?: string; // Command to execute for stdio-based servers
args?: string[]; // Arguments for the command
env?: Record<string, string>; // Environment variables
@@ -103,7 +111,7 @@ export interface ServerInfo {
error: string | null; // Error message if any
tools: ToolInfo[]; // List of tools available on the server
client?: Client; // Client instance for communication
transport?: SSEClientTransport | StdioClientTransport; // Transport mechanism used
transport?: SSEClientTransport | StdioClientTransport | StreamableHTTPClientTransport; // Transport mechanism used
createTime: number; // Timestamp of when the server was created
enabled?: boolean; // Flag to indicate if the server is enabled
}