Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
964ab4a5d7 | ||
|
|
b59243e410 | ||
|
|
85c461bbfa | ||
|
|
f477e1f942 | ||
|
|
7feb5b2bcb | ||
|
|
66d592da1c |
4
.github/workflows/build.yml
vendored
@@ -3,8 +3,6 @@ name: Build
|
||||
on:
|
||||
push:
|
||||
tags: ['v*.*.*']
|
||||
schedule:
|
||||
- cron: '0 23 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@@ -45,7 +43,7 @@ jobs:
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
cache-to: type=gha,mode=max,scope=${{ matrix.variant }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
build-args: |
|
||||
INSTALL_EXT=${{ matrix.variant == 'full' && 'true' || 'false' }}
|
||||
|
||||
2
.github/workflows/release.yml
vendored
@@ -16,4 +16,4 @@ jobs:
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
gen_release_notes: true
|
||||
generate_release_notes: true
|
||||
|
||||
14
README.md
@@ -95,6 +95,20 @@ Connect AI clients (e.g., Claude Desktop, Cursor, Cherry Studio) via:
|
||||
http://localhost:3000/sse
|
||||
```
|
||||
|
||||
**Group-Specific Endpoints (Recommended)**:
|
||||
|
||||

|
||||
|
||||
For targeted access to specific server groups, use the group-based SSE endpoint:
|
||||
```
|
||||
http://localhost:3000/sse/{groupId}
|
||||
```
|
||||
|
||||
Where `{groupId}` is the ID of the group you created in the dashboard. This allows you to:
|
||||
- Connect to a specific subset of MCP servers organized by use case
|
||||
- Isolate different AI tools to access only relevant servers
|
||||
- Implement more granular access control for different environments or teams
|
||||
|
||||
## 🧑💻 Local Development
|
||||
|
||||
```bash
|
||||
|
||||
18
README.zh.md
@@ -95,6 +95,20 @@ docker run -p 3000:3000 samanhappy/mcphub
|
||||
http://localhost:3000/sse
|
||||
```
|
||||
|
||||
**基于分组的 SSE 端点(推荐)**:
|
||||
|
||||

|
||||
|
||||
要针对特定服务器分组进行访问,请使用基于分组的 SSE 端点:
|
||||
```
|
||||
http://localhost:3000/sse/{groupId}
|
||||
```
|
||||
|
||||
其中 `{groupId}` 是您在控制面板中创建的分组 ID。这样做可以:
|
||||
- 连接到按用例组织的特定 MCP 服务器子集
|
||||
- 隔离不同的 AI 工具,使其只能访问相关服务器
|
||||
- 为不同环境或团队实现更精细的访问控制
|
||||
|
||||
## 🧑💻 本地开发
|
||||
|
||||
```bash
|
||||
@@ -122,6 +136,10 @@ pnpm dev
|
||||
- Bug 报告与修复
|
||||
- 翻译与建议
|
||||
|
||||
欢迎加入企微交流共建群
|
||||
|
||||
<img src="assets/wegroup.png" width="500">
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
本项目采用 [Apache 2.0 许可证](LICENSE)。
|
||||
|
||||
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
BIN
assets/group.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
assets/group.zh.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 102 KiB |
BIN
assets/wegroup.png
Normal file
|
After Width: | Height: | Size: 67 KiB |
@@ -11,10 +11,10 @@ interface GroupCardProps {
|
||||
onDelete: (groupId: string) => void
|
||||
}
|
||||
|
||||
const GroupCard = ({
|
||||
group,
|
||||
servers,
|
||||
onEdit,
|
||||
const GroupCard = ({
|
||||
group,
|
||||
servers,
|
||||
onEdit,
|
||||
onDelete
|
||||
}: GroupCardProps) => {
|
||||
const { t } = useTranslation()
|
||||
@@ -35,10 +35,31 @@ const GroupCard = ({
|
||||
}
|
||||
|
||||
const copyToClipboard = () => {
|
||||
navigator.clipboard.writeText(group.id).then(() => {
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
})
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
navigator.clipboard.writeText(group.id).then(() => {
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
})
|
||||
} else {
|
||||
// Fallback for HTTP or unsupported clipboard API
|
||||
const textArea = document.createElement('textarea')
|
||||
textArea.value = group.id
|
||||
// Avoid scrolling to bottom
|
||||
textArea.style.position = 'fixed'
|
||||
textArea.style.left = '-9999px'
|
||||
document.body.appendChild(textArea)
|
||||
textArea.focus()
|
||||
textArea.select()
|
||||
try {
|
||||
document.execCommand('copy')
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
} catch (err) {
|
||||
alert(t('common.copyFailed') || 'Copy failed')
|
||||
console.error('Copy to clipboard failed:', err)
|
||||
}
|
||||
document.body.removeChild(textArea)
|
||||
}
|
||||
}
|
||||
|
||||
// Get servers that belong to this group
|
||||
@@ -52,7 +73,7 @@ const GroupCard = ({
|
||||
<h2 className="text-xl font-semibold text-gray-800">{group.name}</h2>
|
||||
<div className="flex items-center ml-3">
|
||||
<span className="text-xs text-gray-500 mr-1">{group.id}</span>
|
||||
<button
|
||||
<button
|
||||
onClick={copyToClipboard}
|
||||
className="p-1 text-gray-400 hover:text-gray-600 transition-colors"
|
||||
title={t('common.copy')}
|
||||
@@ -92,15 +113,14 @@ const GroupCard = ({
|
||||
) : (
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
{groupServers.map(server => (
|
||||
<div
|
||||
<div
|
||||
key={server.name}
|
||||
className="inline-flex items-center px-3 py-1 bg-gray-50 rounded"
|
||||
>
|
||||
<span className="font-medium text-gray-700 text-sm">{server.name}</span>
|
||||
<span className={`ml-2 inline-block h-2 w-2 rounded-full ${
|
||||
server.status === 'connected' ? 'bg-green-500' :
|
||||
server.status === 'connecting' ? 'bg-yellow-500' : 'bg-red-500'
|
||||
}`}></span>
|
||||
<span className={`ml-2 inline-block h-2 w-2 rounded-full ${server.status === 'connected' ? 'bg-green-500' :
|
||||
server.status === 'connecting' ? 'bg-yellow-500' : 'bg-red-500'
|
||||
}`}></span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import express from 'express';
|
||||
import { check } from 'express-validator';
|
||||
import path from 'path';
|
||||
import {
|
||||
getAllServers,
|
||||
getAllSettings,
|
||||
@@ -71,6 +72,10 @@ export const initRoutes = (app: express.Application): void => {
|
||||
], changePassword);
|
||||
|
||||
app.use('/api', router);
|
||||
|
||||
app.get('*', (_req, res) => {
|
||||
res.sendFile(path.join(process.cwd(), 'frontend', 'dist', 'index.html'));
|
||||
});
|
||||
};
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -27,8 +27,14 @@ export const getMcpServer = (): Server => {
|
||||
|
||||
export const notifyToolChanged = async () => {
|
||||
await registerAllTools(currentServer, true);
|
||||
currentServer.sendToolListChanged();
|
||||
console.log('Tool list changed notification sent');
|
||||
currentServer
|
||||
.sendToolListChanged()
|
||||
.catch((error) => {
|
||||
console.error('Failed to send tool list changed notification:', error);
|
||||
})
|
||||
.then(() => {
|
||||
console.log('Tool list changed notification sent successfully');
|
||||
});
|
||||
};
|
||||
|
||||
// Store all server information
|
||||
|
||||