mirror of
https://github.com/samanhappy/mcphub.git
synced 2025-12-24 10:49:35 -05:00
Compare commits
7 Commits
v0.9.11
...
copilot/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9732fccb6 | ||
|
|
7b3d441046 | ||
|
|
55a7d0b183 | ||
|
|
435227cbd4 | ||
|
|
6a59becd8d | ||
|
|
91698a50e3 | ||
|
|
a5d5045832 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -25,4 +25,5 @@ yarn-error.log*
|
||||
*.log
|
||||
coverage/
|
||||
|
||||
data/
|
||||
data/
|
||||
temp-test-config/
|
||||
@@ -1,8 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { execSync } from 'child_process';
|
||||
import { fileURLToPath, pathToFileURL } from 'url';
|
||||
import fs from 'fs';
|
||||
|
||||
// Enable debug logging if needed
|
||||
@@ -90,7 +89,10 @@ checkFrontend(projectRoot);
|
||||
|
||||
// Start the server
|
||||
console.log('🚀 Starting MCPHub server...');
|
||||
import(path.join(projectRoot, 'dist', 'index.js')).catch(err => {
|
||||
const entryPath = path.join(projectRoot, 'dist', 'index.js');
|
||||
// Convert to file:// URL for cross-platform ESM compatibility (required on Windows)
|
||||
const entryUrl = pathToFileURL(entryPath).href;
|
||||
import(entryUrl).catch(err => {
|
||||
console.error('Failed to start MCPHub:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -47,7 +47,8 @@ MCPHub uses several configuration files:
|
||||
},
|
||||
"playwright": {
|
||||
"command": "npx",
|
||||
"args": ["@playwright/mcp@latest", "--headless"]
|
||||
"args": ["@playwright/mcp@latest", "--headless", "--isolated"],
|
||||
"perSession": true
|
||||
},
|
||||
"slack": {
|
||||
"command": "npx",
|
||||
@@ -75,6 +76,42 @@ MCPHub uses several configuration files:
|
||||
| Field | Type | Default | Description |
|
||||
| -------------- | ------- | --------------- | --------------------------- |
|
||||
| `env` | object | `{}` | Environment variables |
|
||||
| `perSession` | boolean | `false` | Create separate server instance per user session (for stateful servers like playwright) |
|
||||
| `enabled` | boolean | `true` | Enable/disable the server |
|
||||
| `timeout` | number | `60000` | Request timeout in milliseconds |
|
||||
| `keepAliveInterval` | number | `60000` | Keep-alive ping interval for SSE servers (ms) |
|
||||
|
||||
## Per-Session Server Instances
|
||||
|
||||
Some MCP servers maintain state that should be isolated between different users. For example, the Playwright server maintains browser sessions that could leak form data or other state between concurrent users.
|
||||
|
||||
To prevent this, you can set `perSession: true` in the server configuration. This creates a separate server instance for each user session instead of sharing a single instance across all users.
|
||||
|
||||
### When to Use Per-Session Servers
|
||||
|
||||
Use `perSession: true` for servers that:
|
||||
- Maintain browser state (like Playwright)
|
||||
- Store user-specific data in memory
|
||||
- Have file handles or database connections that shouldn't be shared
|
||||
- Could cause race conditions when multiple users access simultaneously
|
||||
|
||||
### Example Configuration
|
||||
|
||||
```json
|
||||
{
|
||||
"playwright": {
|
||||
"command": "npx",
|
||||
"args": ["@playwright/mcp@latest", "--headless", "--isolated"],
|
||||
"perSession": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Important Notes:**
|
||||
- Each per-session server instance consumes additional resources (memory, CPU)
|
||||
- Per-session servers are automatically cleaned up when the user session ends
|
||||
- For Playwright, also use the `--isolated` flag to ensure browser contexts are isolated
|
||||
- Not recommended for stateless servers that can safely be shared
|
||||
|
||||
## Common MCP Server Examples
|
||||
|
||||
@@ -101,8 +138,9 @@ MCPHub uses several configuration files:
|
||||
{
|
||||
"playwright": {
|
||||
"command": "npx",
|
||||
"args": ["@playwright/mcp@latest", "--headless"],
|
||||
"args": ["@playwright/mcp@latest", "--headless", "--isolated"],
|
||||
"timeout": 60000,
|
||||
"perSession": true,
|
||||
"env": {
|
||||
"PLAYWRIGHT_BROWSERS_PATH": "/tmp/browsers"
|
||||
}
|
||||
@@ -110,6 +148,8 @@ MCPHub uses several configuration files:
|
||||
}
|
||||
```
|
||||
|
||||
**Note**: The `--isolated` flag ensures each browser session is isolated, and `perSession: true` creates a separate server instance for each user session, preventing state leakage between concurrent users.
|
||||
|
||||
### File and System Servers
|
||||
|
||||
#### Filesystem Server
|
||||
|
||||
@@ -50,8 +50,9 @@ MCPHub 使用几个配置文件:
|
||||
},
|
||||
"playwright": {
|
||||
"command": "npx",
|
||||
"args": ["@playwright/mcp@latest", "--headless"],
|
||||
"timeout": 60000
|
||||
"args": ["@playwright/mcp@latest", "--headless", "--isolated"],
|
||||
"timeout": 60000,
|
||||
"perSession": true
|
||||
},
|
||||
"slack": {
|
||||
"command": "npx",
|
||||
@@ -79,13 +80,48 @@ MCPHub 使用几个配置文件:
|
||||
| 字段 | 类型 | 默认值 | 描述 |
|
||||
| -------------- | ------- | --------------- | ------------------ |
|
||||
| `env` | object | `{}` | 环境变量 |
|
||||
| `perSession` | boolean | `false` | 为每个用户会话创建独立的服务器实例(用于有状态的服务器,如 playwright) |
|
||||
| `enabled` | boolean | `true` | 启用/禁用服务器 |
|
||||
| `timeout` | number | `60000` | 请求超时(毫秒) |
|
||||
| `keepAliveInterval` | number | `60000` | SSE 服务器的保活 ping 间隔(毫秒) |
|
||||
| `cwd` | string | `process.cwd()` | 工作目录 |
|
||||
| `timeout` | number | `30000` | 启动超时(毫秒) |
|
||||
| `restart` | boolean | `true` | 失败时自动重启 |
|
||||
| `maxRestarts` | number | `5` | 最大重启次数 |
|
||||
| `restartDelay` | number | `5000` | 重启间延迟(毫秒) |
|
||||
| `stdio` | string | `pipe` | stdio 配置 |
|
||||
|
||||
## 会话隔离的服务器实例
|
||||
|
||||
某些 MCP 服务器会维护应该在不同用户之间隔离的状态。例如,Playwright 服务器维护可能在并发用户之间泄漏表单数据或其他状态的浏览器会话。
|
||||
|
||||
为了防止这种情况,您可以在服务器配置中设置 `perSession: true`。这将为每个用户会话创建一个单独的服务器实例,而不是在所有用户之间共享单个实例。
|
||||
|
||||
### 何时使用会话隔离的服务器
|
||||
|
||||
对于以下服务器使用 `perSession: true`:
|
||||
- 维护浏览器状态(如 Playwright)
|
||||
- 在内存中存储用户特定数据
|
||||
- 具有不应共享的文件句柄或数据库连接
|
||||
- 当多个用户同时访问时可能导致竞争条件
|
||||
|
||||
### 示例配置
|
||||
|
||||
```json
|
||||
{
|
||||
"playwright": {
|
||||
"command": "npx",
|
||||
"args": ["@playwright/mcp@latest", "--headless", "--isolated"],
|
||||
"perSession": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**重要提示:**
|
||||
- 每个会话隔离的服务器实例都会消耗额外的资源(内存、CPU)
|
||||
- 会话隔离的服务器在用户会话结束时会自动清理
|
||||
- 对于 Playwright,还要使用 `--isolated` 标志以确保浏览器上下文被隔离
|
||||
- 不建议用于可以安全共享的无状态服务器
|
||||
|
||||
## 常见 MCP 服务器示例
|
||||
|
||||
### Web 和 API 服务器
|
||||
@@ -111,8 +147,9 @@ MCPHub 使用几个配置文件:
|
||||
{
|
||||
"playwright": {
|
||||
"command": "npx",
|
||||
"args": ["@playwright/mcp@latest", "--headless"],
|
||||
"args": ["@playwright/mcp@latest", "--headless", "--isolated"],
|
||||
"timeout": 60000,
|
||||
"perSession": true,
|
||||
"env": {
|
||||
"PLAYWRIGHT_BROWSERS_PATH": "/tmp/browsers"
|
||||
}
|
||||
@@ -120,6 +157,8 @@ MCPHub 使用几个配置文件:
|
||||
}
|
||||
```
|
||||
|
||||
**注意**: `--isolated` 标志确保每个浏览器会话是隔离的,而 `perSession: true` 为每个用户会话创建单独的服务器实例,防止并发用户之间的状态泄漏。
|
||||
|
||||
### 文件和系统服务器
|
||||
|
||||
#### 文件系统服务器
|
||||
|
||||
@@ -14,8 +14,10 @@
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"@playwright/mcp@latest",
|
||||
"--headless"
|
||||
]
|
||||
"--headless",
|
||||
"--isolated"
|
||||
],
|
||||
"perSession": true
|
||||
},
|
||||
"fetch": {
|
||||
"command": "uvx",
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
"dotenv-expand": "^12.0.2",
|
||||
"express": "^4.21.2",
|
||||
"express-validator": "^7.2.1",
|
||||
"i18next": "^25.5.0",
|
||||
"i18next-fs-backend": "^2.6.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"multer": "^2.0.2",
|
||||
@@ -99,7 +100,6 @@
|
||||
"clsx": "^2.1.1",
|
||||
"concurrently": "^9.2.0",
|
||||
"eslint": "^8.57.1",
|
||||
"i18next": "^25.5.0",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"jest": "^30.2.0",
|
||||
"jest-environment-node": "^30.0.5",
|
||||
|
||||
59
pnpm-lock.yaml
generated
59
pnpm-lock.yaml
generated
@@ -57,6 +57,9 @@ importers:
|
||||
express-validator:
|
||||
specifier: ^7.2.1
|
||||
version: 7.2.1
|
||||
i18next:
|
||||
specifier: ^25.5.0
|
||||
version: 25.5.0(typescript@5.9.2)
|
||||
i18next-fs-backend:
|
||||
specifier: ^2.6.0
|
||||
version: 2.6.0
|
||||
@@ -169,9 +172,6 @@ importers:
|
||||
eslint:
|
||||
specifier: ^8.57.1
|
||||
version: 8.57.1
|
||||
i18next:
|
||||
specifier: ^25.5.0
|
||||
version: 25.5.0(typescript@5.9.2)
|
||||
i18next-browser-languagedetector:
|
||||
specifier: ^8.2.0
|
||||
version: 8.2.0
|
||||
@@ -693,78 +693,92 @@ packages:
|
||||
resolution: {integrity: sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-arm@1.2.0':
|
||||
resolution: {integrity: sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-ppc64@1.2.0':
|
||||
resolution: {integrity: sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-s390x@1.2.0':
|
||||
resolution: {integrity: sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-x64@1.2.0':
|
||||
resolution: {integrity: sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.2.0':
|
||||
resolution: {integrity: sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.2.0':
|
||||
resolution: {integrity: sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-linux-arm64@0.34.3':
|
||||
resolution: {integrity: sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-arm@0.34.3':
|
||||
resolution: {integrity: sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-ppc64@0.34.3':
|
||||
resolution: {integrity: sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-s390x@0.34.3':
|
||||
resolution: {integrity: sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-x64@0.34.3':
|
||||
resolution: {integrity: sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linuxmusl-arm64@0.34.3':
|
||||
resolution: {integrity: sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-linuxmusl-x64@0.34.3':
|
||||
resolution: {integrity: sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-wasm32@0.34.3':
|
||||
resolution: {integrity: sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==}
|
||||
@@ -956,24 +970,28 @@ packages:
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@next/swc-linux-arm64-musl@15.5.2':
|
||||
resolution: {integrity: sha512-s6N8k8dF9YGc5T01UPQ08yxsK6fUow5gG1/axWc1HVVBYQBgOjca4oUZF7s4p+kwhkB1bDSGR8QznWrFZ/Rt5g==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@next/swc-linux-x64-gnu@15.5.2':
|
||||
resolution: {integrity: sha512-o1RV/KOODQh6dM6ZRJGZbc+MOAHww33Vbs5JC9Mp1gDk8cpEO+cYC/l7rweiEalkSm5/1WGa4zY7xrNwObN4+Q==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@next/swc-linux-x64-musl@15.5.2':
|
||||
resolution: {integrity: sha512-/VUnh7w8RElYZ0IV83nUcP/J4KJ6LLYliiBIri3p3aW2giF+PAVgZb6mk8jbQSB3WlTai8gEmCAr7kptFa1H6g==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@next/swc-win32-arm64-msvc@15.5.2':
|
||||
resolution: {integrity: sha512-sMPyTvRcNKXseNQ/7qRfVRLa0VhR0esmQ29DD6pqvG71+JdVnESJaHPA8t7bc67KD5spP3+DOCNLhqlEI2ZgQg==}
|
||||
@@ -1191,56 +1209,67 @@ packages:
|
||||
resolution: {integrity: sha512-54v4okehwl5TaSIkpp97rAHGp7t3ghinRd/vyC1iXqXMfjYUTm7TfYmCzXDoHUPTTf36L8pr0E7YsD3CfB3ZDg==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.50.1':
|
||||
resolution: {integrity: sha512-p/LaFyajPN/0PUHjv8TNyxLiA7RwmDoVY3flXHPSzqrGcIp/c2FjwPPP5++u87DGHtw+5kSH5bCJz0mvXngYxw==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.50.1':
|
||||
resolution: {integrity: sha512-2AbMhFFkTo6Ptna1zO7kAXXDLi7H9fGTbVaIq2AAYO7yzcAsuTNWPHhb2aTA6GPiP+JXh85Y8CiS54iZoj4opw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.50.1':
|
||||
resolution: {integrity: sha512-Cgef+5aZwuvesQNw9eX7g19FfKX5/pQRIyhoXLCiBOrWopjo7ycfB292TX9MDcDijiuIJlx1IzJz3IoCPfqs9w==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-loongarch64-gnu@4.50.1':
|
||||
resolution: {integrity: sha512-RPhTwWMzpYYrHrJAS7CmpdtHNKtt2Ueo+BlLBjfZEhYBhK00OsEqM08/7f+eohiF6poe0YRDDd8nAvwtE/Y62Q==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.50.1':
|
||||
resolution: {integrity: sha512-eSGMVQw9iekut62O7eBdbiccRguuDgiPMsw++BVUg+1K7WjZXHOg/YOT9SWMzPZA+w98G+Fa1VqJgHZOHHnY0Q==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.50.1':
|
||||
resolution: {integrity: sha512-S208ojx8a4ciIPrLgazF6AgdcNJzQE4+S9rsmOmDJkusvctii+ZvEuIC4v/xFqzbuP8yDjn73oBlNDgF6YGSXQ==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-musl@4.50.1':
|
||||
resolution: {integrity: sha512-3Ag8Ls1ggqkGUvSZWYcdgFwriy2lWo+0QlYgEFra/5JGtAd6C5Hw59oojx1DeqcA2Wds2ayRgvJ4qxVTzCHgzg==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.50.1':
|
||||
resolution: {integrity: sha512-t9YrKfaxCYe7l7ldFERE1BRg/4TATxIg+YieHQ966jwvo7ddHJxPj9cNFWLAzhkVsbBvNA4qTbPVNsZKBO4NSg==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.50.1':
|
||||
resolution: {integrity: sha512-MCgtFB2+SVNuQmmjHf+wfI4CMxy3Tk8XjA5Z//A0AKD7QXUYFMQcns91K6dEHBvZPCnhJSyDWLApk40Iq/H3tA==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.50.1':
|
||||
resolution: {integrity: sha512-nEvqG+0jeRmqaUMuwzlfMKwcIVffy/9KGbAGyoa26iu6eSngAYQ512bMXuqqPrlTyfqdlB9FVINs93j534UJrg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-openharmony-arm64@4.50.1':
|
||||
resolution: {integrity: sha512-RDsLm+phmT3MJd9SNxA9MNuEAO/J2fhW8GXk62G/B4G7sLVumNFbRwDL6v5NrESb48k+QMqdGbHgEtfU0LCpbA==}
|
||||
@@ -1301,24 +1330,28 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@swc/core-linux-arm64-musl@1.13.5':
|
||||
resolution: {integrity: sha512-9+ZxFN5GJag4CnYnq6apKTnnezpfJhCumyz0504/JbHLo+Ue+ZtJnf3RhyA9W9TINtLE0bC4hKpWi8ZKoETyOQ==}
|
||||
engines: {node: '>=10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@swc/core-linux-x64-gnu@1.13.5':
|
||||
resolution: {integrity: sha512-WD530qvHrki8Ywt/PloKUjaRKgstQqNGvmZl54g06kA+hqtSE2FTG9gngXr3UJxYu/cNAjJYiBifm7+w4nbHbA==}
|
||||
engines: {node: '>=10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@swc/core-linux-x64-musl@1.13.5':
|
||||
resolution: {integrity: sha512-Luj8y4OFYx4DHNQTWjdIuKTq2f5k6uSXICqx+FSabnXptaOBAbJHNbHT/06JZh6NRUouaf0mYXN0mcsqvkhd7Q==}
|
||||
engines: {node: '>=10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@swc/core-win32-arm64-msvc@1.13.5':
|
||||
resolution: {integrity: sha512-cZ6UpumhF9SDJvv4DA2fo9WIzlNFuKSkZpZmPG1c+4PFSEMy5DFOjBSllCvnqihCabzXzpn6ykCwBmHpy31vQw==}
|
||||
@@ -1438,48 +1471,56 @@ packages:
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tailwindcss/oxide-linux-arm64-gnu@4.1.14':
|
||||
resolution: {integrity: sha512-qaEy0dIZ6d9vyLnmeg24yzA8XuEAD9WjpM5nIM1sUgQ/Zv7cVkharPDQcmm/t/TvXoKo/0knI3me3AGfdx6w1w==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tailwindcss/oxide-linux-arm64-musl@4.1.12':
|
||||
resolution: {integrity: sha512-V8pAM3s8gsrXcCv6kCHSuwyb/gPsd863iT+v1PGXC4fSL/OJqsKhfK//v8P+w9ThKIoqNbEnsZqNy+WDnwQqCA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tailwindcss/oxide-linux-arm64-musl@4.1.14':
|
||||
resolution: {integrity: sha512-ISZjT44s59O8xKsPEIesiIydMG/sCXoMBCqsphDm/WcbnuWLxxb+GcvSIIA5NjUw6F8Tex7s5/LM2yDy8RqYBQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-gnu@4.1.12':
|
||||
resolution: {integrity: sha512-xYfqYLjvm2UQ3TZggTGrwxjYaLB62b1Wiysw/YE3Yqbh86sOMoTn0feF98PonP7LtjsWOWcXEbGqDL7zv0uW8Q==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-gnu@4.1.14':
|
||||
resolution: {integrity: sha512-02c6JhLPJj10L2caH4U0zF8Hji4dOeahmuMl23stk0MU1wfd1OraE7rOloidSF8W5JTHkFdVo/O7uRUJJnUAJg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-musl@4.1.12':
|
||||
resolution: {integrity: sha512-ha0pHPamN+fWZY7GCzz5rKunlv9L5R8kdh+YNvP5awe3LtuXb5nRi/H27GeL2U+TdhDOptU7T6Is7mdwh5Ar3A==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-musl@4.1.14':
|
||||
resolution: {integrity: sha512-TNGeLiN1XS66kQhxHG/7wMeQDOoL0S33x9BgmydbrWAb9Qw0KYdd8o1ifx4HOGDWhVmJ+Ul+JQ7lyknQFilO3Q==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tailwindcss/oxide-wasm32-wasi@4.1.12':
|
||||
resolution: {integrity: sha512-4tSyu3dW+ktzdEpuk6g49KdEangu3eCYoqPhWNsZgUhyegEda3M9rG0/j1GV/JjVVsj+lG7jWAyrTlLzd/WEBg==}
|
||||
@@ -1816,41 +1857,49 @@ packages:
|
||||
resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-arm64-musl@1.11.1':
|
||||
resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@unrs/resolver-binding-linux-ppc64-gnu@1.11.1':
|
||||
resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-riscv64-gnu@1.11.1':
|
||||
resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-riscv64-musl@1.11.1':
|
||||
resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@unrs/resolver-binding-linux-s390x-gnu@1.11.1':
|
||||
resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-x64-gnu@1.11.1':
|
||||
resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-x64-musl@1.11.1':
|
||||
resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@unrs/resolver-binding-wasm32-wasi@1.11.1':
|
||||
resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==}
|
||||
@@ -3197,24 +3246,28 @@ packages:
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-arm64-musl@1.30.1:
|
||||
resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-linux-x64-gnu@1.30.1:
|
||||
resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-x64-musl@1.30.1:
|
||||
resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-win32-arm64-msvc@1.30.1:
|
||||
resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==}
|
||||
|
||||
@@ -42,8 +42,9 @@ export const loadOriginalSettings = (): McpSettings => {
|
||||
|
||||
console.log(`Loaded settings from ${settingsPath}`);
|
||||
return settings;
|
||||
} catch (error) {
|
||||
console.error(`Failed to load settings from ${settingsPath}:`, error);
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.warn(`Failed to load settings from ${settingsPath}:`, errorMessage);
|
||||
const defaultSettings = { mcpServers: {}, users: [] };
|
||||
|
||||
// Cache default settings
|
||||
|
||||
@@ -39,6 +39,7 @@ export class DataServicex implements DataService {
|
||||
const result = { ...all };
|
||||
result.users = newSettings.users;
|
||||
result.systemConfig = newSettings.systemConfig;
|
||||
result.groups = newSettings.groups;
|
||||
return result;
|
||||
} else {
|
||||
const result = JSON.parse(JSON.stringify(all));
|
||||
|
||||
@@ -24,6 +24,10 @@ import { getServerDao, ServerConfigWithName } from '../dao/index.js';
|
||||
|
||||
const servers: { [sessionId: string]: Server } = {};
|
||||
|
||||
// Per-session server instances for servers with perSession=true
|
||||
// Key format: `${sessionId}:${serverName}`
|
||||
const perSessionServerInfos: { [key: string]: ServerInfo } = {};
|
||||
|
||||
const serverDao = getServerDao();
|
||||
|
||||
// Helper function to set up keep-alive ping for SSE connections
|
||||
@@ -79,6 +83,8 @@ export const getMcpServer = (sessionId?: string, group?: string): Server => {
|
||||
|
||||
export const deleteMcpServer = (sessionId: string): void => {
|
||||
delete servers[sessionId];
|
||||
// Clean up any per-session servers for this session
|
||||
cleanupPerSessionServers(sessionId);
|
||||
};
|
||||
|
||||
export const notifyToolChanged = async (name?: string) => {
|
||||
@@ -223,6 +229,144 @@ const createTransportFromConfig = (name: string, conf: ServerConfig): any => {
|
||||
return transport;
|
||||
};
|
||||
|
||||
// Helper function to get or create per-session server instance
|
||||
export const getOrCreatePerSessionServer = async (
|
||||
sessionId: string,
|
||||
serverName: string,
|
||||
serverConfig: ServerConfig,
|
||||
): Promise<ServerInfo> => {
|
||||
const key = `${sessionId}:${serverName}`;
|
||||
|
||||
// Return existing session server if it exists
|
||||
if (perSessionServerInfos[key]) {
|
||||
return perSessionServerInfos[key];
|
||||
}
|
||||
|
||||
console.log(`Creating per-session server instance for session ${sessionId}, server ${serverName}`);
|
||||
|
||||
// Create new transport for this session
|
||||
const transport = createTransportFromConfig(serverName, serverConfig);
|
||||
|
||||
const client = new Client(
|
||||
{
|
||||
name: `mcp-client-${serverName}-${sessionId}`,
|
||||
version: '1.0.0',
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
prompts: {},
|
||||
resources: {},
|
||||
tools: {},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// Get request options from server configuration, with fallbacks
|
||||
const serverRequestOptions = serverConfig.options || {};
|
||||
const requestOptions = {
|
||||
timeout: serverRequestOptions.timeout || 60000,
|
||||
resetTimeoutOnProgress: serverRequestOptions.resetTimeoutOnProgress || false,
|
||||
maxTotalTimeout: serverRequestOptions.maxTotalTimeout,
|
||||
};
|
||||
|
||||
// Create server info for this session
|
||||
const serverInfo: ServerInfo = {
|
||||
name: serverName,
|
||||
owner: serverConfig.owner,
|
||||
status: 'connecting',
|
||||
error: null,
|
||||
tools: [],
|
||||
prompts: [],
|
||||
client,
|
||||
transport,
|
||||
options: requestOptions,
|
||||
createTime: Date.now(),
|
||||
config: serverConfig,
|
||||
sessionId: sessionId,
|
||||
};
|
||||
|
||||
perSessionServerInfos[key] = serverInfo;
|
||||
|
||||
// Connect asynchronously
|
||||
client
|
||||
.connect(transport, requestOptions)
|
||||
.then(() => {
|
||||
console.log(`Successfully connected per-session client for server: ${serverName}, session: ${sessionId}`);
|
||||
const capabilities = client.getServerCapabilities();
|
||||
|
||||
if (capabilities?.tools) {
|
||||
client
|
||||
.listTools({}, requestOptions)
|
||||
.then((tools) => {
|
||||
console.log(`Successfully listed ${tools.tools.length} tools for per-session server: ${serverName}, session: ${sessionId}`);
|
||||
serverInfo.tools = tools.tools.map((tool) => ({
|
||||
name: `${serverName}${getNameSeparator()}${tool.name}`,
|
||||
description: tool.description || '',
|
||||
inputSchema: cleanInputSchema(tool.inputSchema || {}),
|
||||
}));
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(`Failed to list tools for per-session server ${serverName}, session ${sessionId}:`, error);
|
||||
});
|
||||
}
|
||||
|
||||
if (capabilities?.prompts) {
|
||||
client
|
||||
.listPrompts({}, requestOptions)
|
||||
.then((prompts) => {
|
||||
console.log(`Successfully listed ${prompts.prompts.length} prompts for per-session server: ${serverName}, session: ${sessionId}`);
|
||||
serverInfo.prompts = prompts.prompts.map((prompt) => ({
|
||||
name: `${serverName}${getNameSeparator()}${prompt.name}`,
|
||||
title: prompt.title,
|
||||
description: prompt.description,
|
||||
arguments: prompt.arguments,
|
||||
}));
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(`Failed to list prompts for per-session server ${serverName}, session ${sessionId}:`, error);
|
||||
});
|
||||
}
|
||||
|
||||
serverInfo.status = 'connected';
|
||||
serverInfo.error = null;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(`Failed to connect per-session client for server ${serverName}, session ${sessionId}:`, error);
|
||||
serverInfo.status = 'disconnected';
|
||||
serverInfo.error = `Failed to connect: ${error.stack}`;
|
||||
});
|
||||
|
||||
return serverInfo;
|
||||
};
|
||||
|
||||
// Helper function to clean up per-session servers for a session
|
||||
export const cleanupPerSessionServers = (sessionId: string): void => {
|
||||
const keysToDelete: string[] = [];
|
||||
|
||||
for (const key in perSessionServerInfos) {
|
||||
if (key.startsWith(`${sessionId}:`)) {
|
||||
const serverInfo = perSessionServerInfos[key];
|
||||
try {
|
||||
if (serverInfo.client) {
|
||||
serverInfo.client.close();
|
||||
}
|
||||
if (serverInfo.transport) {
|
||||
serverInfo.transport.close();
|
||||
}
|
||||
if (serverInfo.keepAliveIntervalId) {
|
||||
clearInterval(serverInfo.keepAliveIntervalId);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Error closing per-session server ${key}:`, error);
|
||||
}
|
||||
keysToDelete.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
keysToDelete.forEach(key => delete perSessionServerInfos[key]);
|
||||
console.log(`Cleaned up ${keysToDelete.length} per-session servers for session ${sessionId}`);
|
||||
};
|
||||
|
||||
// Helper function to handle client.callTool with reconnection logic
|
||||
const callToolWithReconnect = async (
|
||||
serverInfo: ServerInfo,
|
||||
@@ -625,6 +769,45 @@ export const getServerByName = (name: string): ServerInfo | undefined => {
|
||||
return serverInfos.find((serverInfo) => serverInfo.name === name);
|
||||
};
|
||||
|
||||
// Get server by name with session support (for per-session servers)
|
||||
const getServerByNameWithSession = async (name: string, sessionId?: string): Promise<ServerInfo | undefined> => {
|
||||
// First check if this server is configured for per-session instances
|
||||
const serverConfig = await serverDao.findById(name);
|
||||
|
||||
if (serverConfig?.perSession && sessionId) {
|
||||
// Try to get or create per-session server
|
||||
const key = `${sessionId}:${name}`;
|
||||
if (perSessionServerInfos[key]) {
|
||||
return perSessionServerInfos[key];
|
||||
}
|
||||
// Create new per-session server instance
|
||||
return await getOrCreatePerSessionServer(sessionId, name, serverConfig);
|
||||
}
|
||||
|
||||
// Fall back to shared server
|
||||
return serverInfos.find((serverInfo) => serverInfo.name === name && !serverInfo.sessionId);
|
||||
};
|
||||
|
||||
// Get server by tool name with session support (for per-session servers)
|
||||
const getServerByToolWithSession = async (toolName: string, sessionId?: string): Promise<ServerInfo | undefined> => {
|
||||
// First try to find in per-session servers if sessionId is provided
|
||||
if (sessionId) {
|
||||
for (const key in perSessionServerInfos) {
|
||||
if (key.startsWith(`${sessionId}:`)) {
|
||||
const serverInfo = perSessionServerInfos[key];
|
||||
if (serverInfo.tools.some((tool) => tool.name === toolName)) {
|
||||
return serverInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to shared servers
|
||||
return serverInfos.find((serverInfo) =>
|
||||
!serverInfo.sessionId && serverInfo.tools.some((tool) => tool.name === toolName)
|
||||
);
|
||||
};
|
||||
|
||||
// Filter tools by server configuration
|
||||
const filterToolsByConfig = async (serverName: string, tools: Tool[]): Promise<Tool[]> => {
|
||||
const serverConfig = await serverDao.findById(serverName);
|
||||
@@ -640,8 +823,8 @@ const filterToolsByConfig = async (serverName: string, tools: Tool[]): Promise<T
|
||||
});
|
||||
};
|
||||
|
||||
// Get server by tool name
|
||||
const getServerByTool = (toolName: string): ServerInfo | undefined => {
|
||||
// Get server by tool name (legacy - use getServerByToolWithSession instead)
|
||||
const _getServerByTool = (toolName: string): ServerInfo | undefined => {
|
||||
return serverInfos.find((serverInfo) => serverInfo.tools.some((tool) => tool.name === toolName));
|
||||
};
|
||||
|
||||
@@ -826,15 +1009,35 @@ Available servers: ${serversList}`;
|
||||
};
|
||||
}
|
||||
|
||||
const allServerInfos = getDataService()
|
||||
// Get shared servers
|
||||
let allServerInfos = getDataService()
|
||||
.filterData(serverInfos)
|
||||
.filter((serverInfo) => {
|
||||
if (serverInfo.enabled === false) return false;
|
||||
if (serverInfo.sessionId) return false; // Exclude per-session servers from shared list
|
||||
if (!group) return true;
|
||||
const serversInGroup = getServersInGroup(group);
|
||||
if (!serversInGroup || serversInGroup.length === 0) return serverInfo.name === group;
|
||||
return serversInGroup.includes(serverInfo.name);
|
||||
});
|
||||
|
||||
// Add per-session servers for this session
|
||||
if (sessionId) {
|
||||
const sessionServers = Object.values(perSessionServerInfos).filter(
|
||||
(serverInfo) => serverInfo.sessionId === sessionId && serverInfo.status === 'connected'
|
||||
);
|
||||
|
||||
// Filter session servers by group if applicable
|
||||
const filteredSessionServers = sessionServers.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);
|
||||
});
|
||||
|
||||
allServerInfos = [...allServerInfos, ...filteredSessionServers];
|
||||
}
|
||||
|
||||
const allTools = [];
|
||||
for (const serverInfo of allServerInfos) {
|
||||
@@ -998,17 +1201,13 @@ export const handleCallToolRequest = async (request: any, extra: any) => {
|
||||
}
|
||||
|
||||
const { arguments: toolArgs = {} } = request.params.arguments || {};
|
||||
const sessionId = extra?.sessionId;
|
||||
let targetServerInfo: ServerInfo | undefined;
|
||||
if (extra && extra.server) {
|
||||
targetServerInfo = getServerByName(extra.server);
|
||||
targetServerInfo = await getServerByNameWithSession(extra.server, sessionId);
|
||||
} else {
|
||||
// Find the first server that has this tool
|
||||
targetServerInfo = serverInfos.find(
|
||||
(serverInfo) =>
|
||||
serverInfo.status === 'connected' &&
|
||||
serverInfo.enabled !== false &&
|
||||
serverInfo.tools.some((tool) => tool.name === toolName),
|
||||
);
|
||||
// Find the first server that has this tool (session-aware)
|
||||
targetServerInfo = await getServerByToolWithSession(toolName, sessionId);
|
||||
}
|
||||
|
||||
if (!targetServerInfo) {
|
||||
@@ -1114,7 +1313,8 @@ export const handleCallToolRequest = async (request: any, extra: any) => {
|
||||
}
|
||||
|
||||
// Regular tool handling
|
||||
const serverInfo = getServerByTool(request.params.name);
|
||||
const sessionId = extra?.sessionId;
|
||||
const serverInfo = await getServerByToolWithSession(request.params.name, sessionId);
|
||||
if (!serverInfo) {
|
||||
throw new Error(`Server not found: ${request.params.name}`);
|
||||
}
|
||||
|
||||
@@ -43,7 +43,6 @@ export function registerService<T>(key: string, entry: Service<T>) {
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Service registered: ${key} with entry:`, entry);
|
||||
registry.set(key, entry);
|
||||
}
|
||||
|
||||
|
||||
@@ -178,6 +178,7 @@ export interface ServerConfig {
|
||||
enabled?: boolean; // Flag to enable/disable the server
|
||||
owner?: string; // Owner of the server, defaults to 'admin' user
|
||||
keepAliveInterval?: number; // Keep-alive ping interval in milliseconds (default: 60000ms for SSE servers)
|
||||
perSession?: boolean; // If true, creates a separate server instance for each session (useful for stateful servers like playwright)
|
||||
tools?: Record<string, { enabled: boolean; description?: string }>; // Tool-specific configurations with enable/disable state and custom descriptions
|
||||
prompts?: Record<string, { enabled: boolean; description?: string }>; // Prompt-specific configurations with enable/disable state and custom descriptions
|
||||
options?: Partial<Pick<RequestOptions, 'timeout' | 'resetTimeoutOnProgress' | 'maxTotalTimeout'>>; // MCP request options configuration
|
||||
@@ -239,6 +240,7 @@ export interface ServerInfo {
|
||||
enabled?: boolean; // Flag to indicate if the server is enabled
|
||||
keepAliveIntervalId?: NodeJS.Timeout; // Timer ID for keep-alive ping interval
|
||||
config?: ServerConfig; // Reference to the original server configuration for OpenAPI passthrough headers
|
||||
sessionId?: string; // Session ID for per-session server instances (undefined for shared servers)
|
||||
}
|
||||
|
||||
// Details about a tool available on the server
|
||||
|
||||
@@ -5,6 +5,13 @@ import { dirname } from 'path';
|
||||
// Project root directory - use process.cwd() as a simpler alternative
|
||||
const rootDir = process.cwd();
|
||||
|
||||
function getParentPath(p: string, filename: string): string {
|
||||
if (p.endsWith(filename)) {
|
||||
p = p.slice(0, -filename.length);
|
||||
}
|
||||
return path.resolve(p);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the path to a configuration file by checking multiple potential locations.
|
||||
* @param filename The name of the file to locate (e.g., 'servers.json', 'mcp_settings.json')
|
||||
@@ -12,15 +19,35 @@ const rootDir = process.cwd();
|
||||
* @returns The path to the file
|
||||
*/
|
||||
export const getConfigFilePath = (filename: string, description = 'Configuration'): string => {
|
||||
const envPath = process.env.MCPHUB_SETTING_PATH;
|
||||
if (filename === 'mcp_settings.json') {
|
||||
const envPath = process.env.MCPHUB_SETTING_PATH;
|
||||
if (envPath) {
|
||||
// Ensure directory exists
|
||||
const dir = getParentPath(envPath, filename);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
console.log(`Created directory for settings at ${dir}`);
|
||||
}
|
||||
|
||||
// if full path, return as is
|
||||
if (envPath?.endsWith(filename)) {
|
||||
return envPath;
|
||||
}
|
||||
|
||||
// if directory, return path under that directory
|
||||
return path.resolve(envPath, filename);
|
||||
}
|
||||
}
|
||||
|
||||
const potentialPaths = [
|
||||
...(envPath ? [envPath] : []),
|
||||
// Prioritize process.cwd() as the first location to check
|
||||
path.resolve(process.cwd(), filename),
|
||||
// Use path relative to the root directory
|
||||
path.join(rootDir, filename),
|
||||
// If installed with npx, may need to look one level up
|
||||
path.join(dirname(rootDir), filename),
|
||||
...[
|
||||
// Prioritize process.cwd() as the first location to check
|
||||
path.resolve(process.cwd(), filename),
|
||||
// Use path relative to the root directory
|
||||
path.join(rootDir, filename),
|
||||
// If installed with npx, may need to look one level up
|
||||
path.join(dirname(rootDir), filename),
|
||||
],
|
||||
];
|
||||
|
||||
for (const filePath of potentialPaths) {
|
||||
|
||||
111
tests/services/perSessionServers.test.ts
Normal file
111
tests/services/perSessionServers.test.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import {
|
||||
getOrCreatePerSessionServer,
|
||||
cleanupPerSessionServers,
|
||||
} from '../../src/services/mcpService';
|
||||
import { ServerConfig } from '../../src/types';
|
||||
|
||||
// Mock the serverDao
|
||||
jest.mock('../../src/dao/index.js', () => ({
|
||||
getServerDao: () => ({
|
||||
findById: jest.fn((name: string) => {
|
||||
if (name === 'playwright') {
|
||||
return Promise.resolve({
|
||||
name: 'playwright',
|
||||
command: 'npx',
|
||||
args: ['@playwright/mcp@latest', '--headless', '--isolated'],
|
||||
perSession: true,
|
||||
enabled: true,
|
||||
});
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
}),
|
||||
findAll: jest.fn(() => Promise.resolve([])),
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock the Client and Transport classes
|
||||
jest.mock('@modelcontextprotocol/sdk/client/index.js', () => ({
|
||||
Client: jest.fn().mockImplementation(() => ({
|
||||
connect: jest.fn(() => Promise.resolve()),
|
||||
close: jest.fn(),
|
||||
listTools: jest.fn(() => Promise.resolve({ tools: [] })),
|
||||
listPrompts: jest.fn(() => Promise.resolve({ prompts: [] })),
|
||||
getServerCapabilities: jest.fn(() => ({ tools: true, prompts: true })),
|
||||
callTool: jest.fn((params) => Promise.resolve({ content: [{ type: 'text', text: `Tool ${params.name} called` }] })),
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('@modelcontextprotocol/sdk/client/stdio.js', () => ({
|
||||
StdioClientTransport: jest.fn().mockImplementation(() => ({
|
||||
close: jest.fn(),
|
||||
stderr: {
|
||||
on: jest.fn(),
|
||||
},
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('Per-Session Server Instances', () => {
|
||||
afterEach(() => {
|
||||
// Clean up any created sessions
|
||||
cleanupPerSessionServers('session1');
|
||||
cleanupPerSessionServers('session2');
|
||||
});
|
||||
|
||||
it('should create separate server instances for different sessions', async () => {
|
||||
const config: ServerConfig = {
|
||||
command: 'npx',
|
||||
args: ['@playwright/mcp@latest', '--headless', '--isolated'],
|
||||
perSession: true,
|
||||
};
|
||||
|
||||
// Create server for session1
|
||||
const server1 = await getOrCreatePerSessionServer('session1', 'playwright', config);
|
||||
expect(server1).toBeDefined();
|
||||
expect(server1.sessionId).toBe('session1');
|
||||
|
||||
// Create server for session2
|
||||
const server2 = await getOrCreatePerSessionServer('session2', 'playwright', config);
|
||||
expect(server2).toBeDefined();
|
||||
expect(server2.sessionId).toBe('session2');
|
||||
|
||||
// They should be different instances
|
||||
expect(server1).not.toBe(server2);
|
||||
});
|
||||
|
||||
it('should reuse existing per-session server for the same session', async () => {
|
||||
const config: ServerConfig = {
|
||||
command: 'npx',
|
||||
args: ['@playwright/mcp@latest', '--headless', '--isolated'],
|
||||
perSession: true,
|
||||
};
|
||||
|
||||
// Create server for session1
|
||||
const server1 = await getOrCreatePerSessionServer('session1', 'playwright', config);
|
||||
|
||||
// Request the same server again
|
||||
const server2 = await getOrCreatePerSessionServer('session1', 'playwright', config);
|
||||
|
||||
// Should be the same instance
|
||||
expect(server1).toBe(server2);
|
||||
});
|
||||
|
||||
it('should clean up per-session servers when session ends', async () => {
|
||||
const config: ServerConfig = {
|
||||
command: 'npx',
|
||||
args: ['@playwright/mcp@latest', '--headless', '--isolated'],
|
||||
perSession: true,
|
||||
};
|
||||
|
||||
// Create server for session1
|
||||
const server1 = await getOrCreatePerSessionServer('session1', 'playwright', config);
|
||||
expect(server1).toBeDefined();
|
||||
|
||||
// Clean up session1
|
||||
cleanupPerSessionServers('session1');
|
||||
|
||||
// Create again should create a new instance (not the same object)
|
||||
const server2 = await getOrCreatePerSessionServer('session1', 'playwright', config);
|
||||
expect(server2).toBeDefined();
|
||||
expect(server2).not.toBe(server1);
|
||||
});
|
||||
});
|
||||
131
tests/utils/cliPathHandling.test.ts
Normal file
131
tests/utils/cliPathHandling.test.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
// Test for CLI path handling functionality
|
||||
import path from 'path';
|
||||
import { pathToFileURL } from 'url';
|
||||
|
||||
describe('CLI Path Handling', () => {
|
||||
describe('Cross-platform ESM URL conversion', () => {
|
||||
it('should convert Unix-style absolute path to file:// URL', () => {
|
||||
const unixPath = '/home/user/project/dist/index.js';
|
||||
const fileUrl = pathToFileURL(unixPath).href;
|
||||
|
||||
expect(fileUrl).toMatch(/^file:\/\//);
|
||||
expect(fileUrl).toContain('index.js');
|
||||
});
|
||||
|
||||
it('should handle relative paths correctly', () => {
|
||||
const relativePath = path.join(process.cwd(), 'dist', 'index.js');
|
||||
const fileUrl = pathToFileURL(relativePath).href;
|
||||
|
||||
expect(fileUrl).toMatch(/^file:\/\//);
|
||||
expect(fileUrl).toContain('dist');
|
||||
expect(fileUrl).toContain('index.js');
|
||||
});
|
||||
|
||||
it('should produce valid URL format', () => {
|
||||
const testPath = path.join(process.cwd(), 'test', 'file.js');
|
||||
const fileUrl = pathToFileURL(testPath).href;
|
||||
|
||||
// Should be a valid URL
|
||||
expect(() => new URL(fileUrl)).not.toThrow();
|
||||
|
||||
// Should start with file://
|
||||
expect(fileUrl.startsWith('file://')).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle paths with spaces', () => {
|
||||
const pathWithSpaces = path.join(process.cwd(), 'my folder', 'dist', 'index.js');
|
||||
const fileUrl = pathToFileURL(pathWithSpaces).href;
|
||||
|
||||
expect(fileUrl).toMatch(/^file:\/\//);
|
||||
expect(fileUrl).toContain('index.js');
|
||||
// Spaces should be URL-encoded
|
||||
expect(fileUrl).toContain('%20');
|
||||
});
|
||||
|
||||
it('should handle paths with special characters', () => {
|
||||
const pathWithSpecialChars = path.join(process.cwd(), 'test@dir', 'file#1.js');
|
||||
const fileUrl = pathToFileURL(pathWithSpecialChars).href;
|
||||
|
||||
expect(fileUrl).toMatch(/^file:\/\//);
|
||||
// Special characters should be URL-encoded
|
||||
expect(() => new URL(fileUrl)).not.toThrow();
|
||||
});
|
||||
|
||||
// Windows-specific path handling simulation
|
||||
it('should handle Windows-style paths correctly', () => {
|
||||
// Simulate a Windows path structure
|
||||
// Note: On non-Windows systems, this creates a relative path,
|
||||
// but the test verifies the conversion mechanism works
|
||||
const mockWindowsPath = 'C:\\Users\\User\\project\\dist\\index.js';
|
||||
|
||||
// On Windows, pathToFileURL would convert C:\ to file:///C:/
|
||||
// On Unix, it treats it as a relative path, but the conversion still works
|
||||
const fileUrl = pathToFileURL(mockWindowsPath).href;
|
||||
|
||||
expect(fileUrl).toMatch(/^file:\/\//);
|
||||
expect(fileUrl).toContain('index.js');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Path normalization', () => {
|
||||
it('should normalize path separators', () => {
|
||||
const mixedPath = path.join('dist', 'index.js');
|
||||
const fileUrl = pathToFileURL(path.resolve(mixedPath)).href;
|
||||
|
||||
expect(fileUrl).toMatch(/^file:\/\//);
|
||||
// All separators should be forward slashes in URL
|
||||
expect(fileUrl.split('file://')[1]).not.toContain('\\');
|
||||
});
|
||||
|
||||
it('should handle multiple consecutive slashes', () => {
|
||||
const messyPath = path.normalize('/dist//index.js');
|
||||
const fileUrl = pathToFileURL(path.resolve(messyPath)).href;
|
||||
|
||||
expect(fileUrl).toMatch(/^file:\/\//);
|
||||
expect(() => new URL(fileUrl)).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Path resolution for CLI use case', () => {
|
||||
it('should convert package root path to valid import URL', () => {
|
||||
const packageRoot = process.cwd();
|
||||
const entryPath = path.join(packageRoot, 'dist', 'index.js');
|
||||
const entryUrl = pathToFileURL(entryPath).href;
|
||||
|
||||
expect(entryUrl).toMatch(/^file:\/\//);
|
||||
expect(entryUrl).toContain('dist');
|
||||
expect(entryUrl).toContain('index.js');
|
||||
expect(() => new URL(entryUrl)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should handle nested directory structures', () => {
|
||||
const deepPath = path.join(process.cwd(), 'a', 'b', 'c', 'd', 'file.js');
|
||||
const fileUrl = pathToFileURL(deepPath).href;
|
||||
|
||||
expect(fileUrl).toMatch(/^file:\/\//);
|
||||
expect(fileUrl).toContain('file.js');
|
||||
expect(() => new URL(fileUrl)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should produce URL compatible with dynamic import()', () => {
|
||||
// This test verifies the exact pattern used in bin/cli.js
|
||||
const projectRoot = process.cwd();
|
||||
const entryPath = path.join(projectRoot, 'dist', 'index.js');
|
||||
const entryUrl = pathToFileURL(entryPath).href;
|
||||
|
||||
// The URL should be valid for import()
|
||||
expect(entryUrl).toMatch(/^file:\/\//);
|
||||
expect(typeof entryUrl).toBe('string');
|
||||
|
||||
// Verify the URL format is valid
|
||||
const urlObj = new URL(entryUrl);
|
||||
expect(urlObj.protocol).toBe('file:');
|
||||
expect(urlObj.href).toBe(entryUrl);
|
||||
|
||||
// On Windows, pathToFileURL converts 'C:\path' to 'file:///C:/path'
|
||||
// On Unix, it converts '/path' to 'file:///path'
|
||||
// Both formats are valid for dynamic import()
|
||||
expect(entryUrl).toContain('index.js');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user