Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39222bdcd8 | ||
|
|
436318b24c | ||
|
|
3e5a64d533 | ||
|
|
6988618c41 | ||
|
|
964ab4a5d7 | ||
|
|
b59243e410 | ||
|
|
85c461bbfa | ||
|
|
f477e1f942 | ||
|
|
7feb5b2bcb |
75
.github/workflows/build.yml
vendored
@@ -3,16 +3,11 @@ name: Build
|
||||
on:
|
||||
push:
|
||||
tags: ['v*.*.*']
|
||||
schedule:
|
||||
- cron: '0 23 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
build-base:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
variant: ${{ startsWith(github.ref, 'refs/tags/') && fromJSON('["base", "full"]') || fromJSON('["base"]') }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -33,19 +28,73 @@ jobs:
|
||||
with:
|
||||
images: samanhappy/mcphub
|
||||
tags: |
|
||||
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/') }}
|
||||
# edge 变体
|
||||
type=raw,value=edge,enable=${{ github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' }}
|
||||
|
||||
- name: Build and Push Docker Image
|
||||
# semver
|
||||
type=semver,pattern={{version}},enable=${{ startsWith(github.ref, 'refs/tags/') }}
|
||||
|
||||
# latest
|
||||
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/') }}
|
||||
|
||||
- name: Build and Push Base Docker Image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
cache-from: type=gha,scope=base-cache
|
||||
cache-to: type=gha,mode=max,scope=base-cache
|
||||
platforms: linux/amd64,linux/arm64
|
||||
build-args: |
|
||||
INSTALL_EXT=${{ matrix.variant == 'full' && 'true' || 'false' }}
|
||||
INSTALL_EXT=false
|
||||
# 确保构建完全独立,不共享缓存或中间层
|
||||
provenance: false
|
||||
no-cache: true
|
||||
|
||||
build-full:
|
||||
needs: build-base
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/') }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: samanhappy/mcphub
|
||||
tags: |
|
||||
# semver with full suffix
|
||||
type=semver,pattern={{version}}-full,enable=${{ startsWith(github.ref, 'refs/tags/') }}
|
||||
|
||||
# latest-full
|
||||
type=raw,value=latest-full,enable=${{ startsWith(github.ref, 'refs/tags/') }}
|
||||
|
||||
- name: Build and Push Full Docker Image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha,scope=full-cache
|
||||
cache-to: type=gha,mode=max,scope=full-cache
|
||||
platforms: linux/amd64,linux/arm64
|
||||
build-args: |
|
||||
INSTALL_EXT=true
|
||||
# 确保构建完全独立,不共享缓存或中间层
|
||||
provenance: false
|
||||
no-cache: 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;
|
||||
|
||||