Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4008f617d | ||
|
|
6a5039a90e | ||
|
|
39222bdcd8 | ||
|
|
436318b24c | ||
|
|
3e5a64d533 | ||
|
|
6988618c41 | ||
|
|
964ab4a5d7 | ||
|
|
b59243e410 | ||
|
|
85c461bbfa | ||
|
|
f477e1f942 | ||
|
|
7feb5b2bcb |
6
.github/workflows/build.yml
vendored
@@ -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' }}
|
||||
|
||||
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 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);
|
||||
|
||||