Compare commits

...

11 Commits

11 changed files with 75 additions and 17 deletions

View File

@@ -3,8 +3,6 @@ name: Build
on:
push:
tags: ['v*.*.*']
schedule:
- cron: '0 23 * * *'
workflow_dispatch:
jobs:
@@ -36,6 +34,8 @@ jobs:
type=raw,value=edge${{ matrix.variant == 'full' && '-full' || '' }},enable=${{ github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' }}
type=semver,pattern={{version}}${{ matrix.variant == 'full' && '-full' || '' }},enable=${{ startsWith(github.ref, 'refs/tags/') }}
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
@@ -45,7 +45,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' }}

View File

@@ -95,6 +95,20 @@ Connect AI clients (e.g., Claude Desktop, Cursor, Cherry Studio) via:
http://localhost:3000/sse
```
**Group-Specific Endpoints (Recommended)**:
![Group Management](assets/group.png)
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

View File

@@ -95,6 +95,20 @@ docker run -p 3000:3000 samanhappy/mcphub
http://localhost:3000/sse
```
**基于分组的 SSE 端点(推荐)**
![分组](assets/group.zh.png)
要针对特定服务器分组进行访问,请使用基于分组的 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)。

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

BIN
assets/group.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
assets/group.zh.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

BIN
assets/wegroup.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

View File

@@ -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>

View File

@@ -1,5 +1,6 @@
import express from 'express';
import config from './config/index.js';
import path from 'path';
import { initMcpServer } from './services/mcpService.js';
import { initMiddlewares } from './middlewares/index.js';
import { initRoutes } from './routes/index.js';
@@ -37,6 +38,11 @@ export class AppServer {
.catch((error) => {
console.error('Error initializing MCP server:', error);
throw error;
})
.finally(() => {
this.app.get('*', (_req, res) => {
res.sendFile(path.join(process.cwd(), 'frontend', 'dist', 'index.html'));
});
});
} catch (error) {
console.error('Error initializing server:', error);