mirror of
https://github.com/samanhappy/mcphub.git
synced 2025-12-30 21:49:13 -05:00
Compare commits
5 Commits
copilot/fi
...
copilot/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a9ea9bc4b | ||
|
|
3acdd99664 | ||
|
|
4ac875860c | ||
|
|
7b9e9da7bc | ||
|
|
cd7e2a23a3 |
@@ -57,15 +57,6 @@ Créez un fichier `mcp_settings.json` pour personnaliser les paramètres de votr
|
||||
}
|
||||
```
|
||||
|
||||
#### Exemples de Configuration
|
||||
|
||||
Pour des configurations spécifiques de serveurs MCP, consultez le répertoire [examples](./examples/) :
|
||||
|
||||
- **[Démarrage rapide Jira Cloud](./examples/QUICK_START_JIRA.md)** - Guide de configuration en 5 minutes pour Jira Cloud
|
||||
- **[Guide complet Atlassian/Jira](./examples/README_ATLASSIAN_JIRA.md)** - Configuration détaillée pour Jira et Confluence
|
||||
- **[Variables d'environnement](./examples/mcp_settings_with_env_vars.json)** - Utilisation de variables d'environnement dans la configuration
|
||||
- **[OpenAPI Schema](./examples/openapi-schema-config.json)** - Serveurs MCP basés sur OpenAPI
|
||||
|
||||
### Déploiement avec Docker
|
||||
|
||||
**Recommandé** : Montez votre configuration personnalisée :
|
||||
|
||||
37
README.md
37
README.md
@@ -59,15 +59,6 @@ Create a `mcp_settings.json` file to customize your server settings:
|
||||
}
|
||||
```
|
||||
|
||||
#### Configuration Examples
|
||||
|
||||
For specific MCP server configurations, see the [examples](./examples/) directory:
|
||||
|
||||
- **[Jira Cloud Quick Start](./examples/QUICK_START_JIRA.md)** - 5-minute setup guide for Jira Cloud
|
||||
- **[Atlassian/Jira Complete Guide](./examples/README_ATLASSIAN_JIRA.md)** - Detailed setup for Jira and Confluence
|
||||
- **[Environment Variables](./examples/mcp_settings_with_env_vars.json)** - Using environment variables in configuration
|
||||
- **[OpenAPI Schema](./examples/openapi-schema-config.json)** - OpenAPI-based MCP servers
|
||||
|
||||
#### OAuth Configuration (Optional)
|
||||
|
||||
MCPHub supports OAuth 2.0 for authenticating with upstream MCP servers. See the [OAuth feature guide](docs/features/oauth.mdx) for a full walkthrough. In practice you will run into two configuration patterns:
|
||||
@@ -107,6 +98,34 @@ Manual registration example:
|
||||
|
||||
For manual providers, create the OAuth App in the upstream console, set the redirect URI to `http://localhost:3000/oauth/callback` (or your deployed domain), and then plug the credentials into the dashboard or config file.
|
||||
|
||||
#### Connection Modes (Optional)
|
||||
|
||||
MCPHub supports two connection strategies:
|
||||
|
||||
- **`persistent` (default)**: Maintains long-running connections for stateful servers
|
||||
- **`on-demand`**: Connects only when needed, ideal for ephemeral servers that exit after operations
|
||||
|
||||
Example for one-time use servers:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"pdf-reader": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "pdf-mcp-server"],
|
||||
"connectionMode": "on-demand"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Use `on-demand` mode for servers that:
|
||||
- Don't support long-running connections
|
||||
- Exit automatically after handling requests
|
||||
- Experience "Connection closed" errors
|
||||
|
||||
See the [Configuration Guide](docs/configuration/mcp-settings.mdx) for more details.
|
||||
|
||||
### Docker Deployment
|
||||
|
||||
**Recommended**: Mount your custom config:
|
||||
|
||||
@@ -57,15 +57,6 @@ MCPHub 通过将多个 MCP(Model Context Protocol)服务器组织为灵活
|
||||
}
|
||||
```
|
||||
|
||||
#### 配置示例
|
||||
|
||||
有关特定 MCP 服务器配置,请参阅 [examples](./examples/) 目录:
|
||||
|
||||
- **[Jira Cloud 快速入门](./examples/QUICK_START_JIRA.md)** - Jira Cloud 5 分钟配置指南
|
||||
- **[Atlassian/Jira 完整指南](./examples/README_ATLASSIAN_JIRA.md)** - Jira 和 Confluence 详细设置
|
||||
- **[环境变量](./examples/mcp_settings_with_env_vars.json)** - 在配置中使用环境变量
|
||||
- **[OpenAPI Schema](./examples/openapi-schema-config.json)** - 基于 OpenAPI 的 MCP 服务器
|
||||
|
||||
#### OAuth 配置(可选)
|
||||
|
||||
MCPHub 支持通过 OAuth 2.0 访问上游 MCP 服务器。完整说明请参阅[《OAuth 功能指南》](docs/zh/features/oauth.mdx)。实际使用中通常会遇到两类配置:
|
||||
|
||||
@@ -72,9 +72,13 @@ MCPHub uses several configuration files:
|
||||
|
||||
### Optional Fields
|
||||
|
||||
| Field | Type | Default | Description |
|
||||
| -------------- | ------- | --------------- | --------------------------- |
|
||||
| `env` | object | `{}` | Environment variables |
|
||||
| Field | Type | Default | Description |
|
||||
| ---------------- | ------- | --------------- | --------------------------------------------------------------------- |
|
||||
| `env` | object | `{}` | Environment variables |
|
||||
| `connectionMode` | string | `"persistent"` | Connection strategy: `"persistent"` or `"on-demand"` |
|
||||
| `enabled` | boolean | `true` | Enable/disable the server |
|
||||
| `keepAliveInterval` | number | `60000` | Keep-alive ping interval for SSE connections (milliseconds) |
|
||||
| `options` | object | `{}` | MCP request options (timeout, resetTimeoutOnProgress, maxTotalTimeout)|
|
||||
|
||||
## Common MCP Server Examples
|
||||
|
||||
@@ -207,55 +211,6 @@ MCPHub uses several configuration files:
|
||||
}
|
||||
```
|
||||
|
||||
### Productivity and Project Management
|
||||
|
||||
#### Atlassian (Jira & Confluence) Server
|
||||
|
||||
```json
|
||||
{
|
||||
"atlassian": {
|
||||
"command": "uvx",
|
||||
"args": [
|
||||
"mcp-atlassian",
|
||||
"--jira-url=${JIRA_URL}",
|
||||
"--jira-username=${JIRA_USERNAME}",
|
||||
"--jira-token=${JIRA_TOKEN}",
|
||||
"--confluence-url=${CONFLUENCE_URL}",
|
||||
"--confluence-username=${CONFLUENCE_USERNAME}",
|
||||
"--confluence-token=${CONFLUENCE_TOKEN}"
|
||||
],
|
||||
"env": {}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Jira Cloud Only Configuration:**
|
||||
|
||||
```json
|
||||
{
|
||||
"jira": {
|
||||
"command": "uvx",
|
||||
"args": [
|
||||
"mcp-atlassian",
|
||||
"--jira-url=${JIRA_URL}",
|
||||
"--jira-username=${JIRA_USERNAME}",
|
||||
"--jira-token=${JIRA_TOKEN}"
|
||||
],
|
||||
"env": {}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Required Environment Variables:**
|
||||
- `JIRA_URL`: Your Jira Cloud URL (e.g., `https://your-company.atlassian.net`)
|
||||
- `JIRA_USERNAME`: Your Atlassian account email
|
||||
- `JIRA_TOKEN`: API token from [Atlassian API Tokens](https://id.atlassian.com/manage-profile/security/api-tokens)
|
||||
- `CONFLUENCE_URL`: Your Confluence URL (e.g., `https://your-company.atlassian.net/wiki`)
|
||||
- `CONFLUENCE_USERNAME`: Your Confluence account email (often same as Jira)
|
||||
- `CONFLUENCE_TOKEN`: Confluence API token (can be same as Jira token for Cloud)
|
||||
|
||||
**Setup Guide:** See [examples/README_ATLASSIAN_JIRA.md](../../examples/README_ATLASSIAN_JIRA.md) for detailed setup instructions.
|
||||
|
||||
### Map and Location Services
|
||||
|
||||
#### Amap (高德地图) Server
|
||||
@@ -287,6 +242,68 @@ MCPHub uses several configuration files:
|
||||
}
|
||||
```
|
||||
|
||||
## Connection Modes
|
||||
|
||||
MCPHub supports two connection strategies for MCP servers:
|
||||
|
||||
### Persistent Connection (Default)
|
||||
|
||||
Persistent mode maintains a long-running connection to the MCP server. This is the default and recommended mode for most servers.
|
||||
|
||||
**Use cases:**
|
||||
- Servers that maintain state between requests
|
||||
- Servers with slow startup times
|
||||
- Servers designed for long-running connections
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"github": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-github"],
|
||||
"connectionMode": "persistent",
|
||||
"env": {
|
||||
"GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_TOKEN}"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### On-Demand Connection
|
||||
|
||||
On-demand mode connects only when a tool is invoked, then disconnects immediately after. This is ideal for servers that:
|
||||
- Don't support long-running connections
|
||||
- Are designed for one-time use
|
||||
- Exit automatically after handling requests
|
||||
|
||||
**Use cases:**
|
||||
- PDF processing tools that exit after each operation
|
||||
- One-time command-line utilities
|
||||
- Servers with connection stability issues
|
||||
- Resource-intensive servers that shouldn't run continuously
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"pdf-reader": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "pdf-mcp-server"],
|
||||
"connectionMode": "on-demand",
|
||||
"env": {
|
||||
"PDF_CACHE_DIR": "/tmp/pdf-cache"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits of on-demand mode:**
|
||||
- Avoids "Connection closed" errors for ephemeral services
|
||||
- Reduces resource usage for infrequently used tools
|
||||
- Better suited for stateless operations
|
||||
- Handles servers that automatically exit after operations
|
||||
|
||||
**Note:** On-demand servers briefly connect during initialization to discover available tools, then disconnect. The connection is re-established only when a tool from that server is actually invoked.
|
||||
|
||||
## Advanced Configuration
|
||||
|
||||
### Environment Variable Substitution
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
# Atlassian Jira Cloud Configuration Example
|
||||
# Copy this to your .env file and fill in your actual values
|
||||
|
||||
# Jira Configuration (Required)
|
||||
JIRA_URL=https://your-company.atlassian.net
|
||||
JIRA_USERNAME=your.email@company.com
|
||||
JIRA_TOKEN=your_jira_api_token_here
|
||||
|
||||
# Confluence Configuration (Optional - only if you want to use Confluence)
|
||||
CONFLUENCE_URL=https://your-company.atlassian.net/wiki
|
||||
CONFLUENCE_USERNAME=your.email@company.com
|
||||
CONFLUENCE_TOKEN=your_confluence_api_token_here
|
||||
|
||||
# Notes:
|
||||
# 1. Get your API token from: https://id.atlassian.com/manage-profile/security/api-tokens
|
||||
# 2. For Atlassian Cloud, you can often use the same API token for both Jira and Confluence
|
||||
# 3. The username should be your Atlassian account email address
|
||||
# 4. Never commit your .env file to version control
|
||||
@@ -1,264 +0,0 @@
|
||||
# Atlassian/Jira Configuration Screenshot Guide
|
||||
|
||||
This guide shows what your configuration should look like at each step.
|
||||
|
||||
## 📋 Configuration File Structure
|
||||
|
||||
Your `mcp_settings.json` should look like this:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"jira": {
|
||||
"command": "uvx",
|
||||
"args": [
|
||||
"mcp-atlassian",
|
||||
"--jira-url=${JIRA_URL}",
|
||||
"--jira-username=${JIRA_USERNAME}",
|
||||
"--jira-token=${JIRA_TOKEN}"
|
||||
]
|
||||
}
|
||||
},
|
||||
"users": [
|
||||
{
|
||||
"username": "admin",
|
||||
"password": "${ADMIN_PASSWORD_HASH}",
|
||||
"isAdmin": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Security Note:** The `password` field must contain a bcrypt hash, not plain text.
|
||||
|
||||
**To generate a bcrypt hash:**
|
||||
```bash
|
||||
node -e "console.log(require('bcrypt').hashSync('your-password', 10))"
|
||||
```
|
||||
|
||||
**⚠️ CRITICAL SECURITY:**
|
||||
- Never use default credentials in production
|
||||
- Always change the admin password before deploying
|
||||
- Store password hashes, never plain text passwords
|
||||
|
||||
## 📁 File Structure
|
||||
|
||||
Your project should have these files:
|
||||
|
||||
```
|
||||
mcphub/
|
||||
├── mcp_settings.json ← Your configuration file
|
||||
├── .env ← Your environment variables (DO NOT COMMIT!)
|
||||
├── data/ ← Database directory (auto-created)
|
||||
└── ...
|
||||
```
|
||||
|
||||
## 🔐 Environment Variables (.env file)
|
||||
|
||||
```env
|
||||
# .env file content
|
||||
JIRA_URL=https://mycompany.atlassian.net
|
||||
JIRA_USERNAME=myemail@company.com
|
||||
JIRA_TOKEN=ATBBxxxxxxxxxxxxxxxxxxx
|
||||
```
|
||||
|
||||
## 🎯 Expected Dashboard View
|
||||
|
||||
After starting MCPHub, you should see:
|
||||
|
||||
### 1. Server List View
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ MCP Servers │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ✅ jira │
|
||||
│ Status: Connected │
|
||||
│ Type: stdio │
|
||||
│ Command: uvx mcp-atlassian │
|
||||
│ Tools: 15 available │
|
||||
│ │
|
||||
│ [View Details] [Restart] [Stop] │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2. Server Details View
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Server: jira │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Status: ✅ Connected │
|
||||
│ Type: stdio │
|
||||
│ Command: uvx │
|
||||
│ │
|
||||
│ Available Tools: │
|
||||
│ • jira_search_issues │
|
||||
│ • jira_get_issue │
|
||||
│ • jira_list_projects │
|
||||
│ • jira_get_project │
|
||||
│ • ... and 11 more │
|
||||
│ │
|
||||
│ Logs: │
|
||||
│ [INFO] Successfully connected to Jira │
|
||||
│ [INFO] Loaded 15 tools │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3. Connection Endpoints
|
||||
|
||||
Once connected, your Jira server is available at:
|
||||
|
||||
| Endpoint | URL | Description |
|
||||
|----------|-----|-------------|
|
||||
| All Servers | `http://localhost:3000/mcp` | Access all configured MCP servers |
|
||||
| Jira Only | `http://localhost:3000/mcp/jira` | Direct access to Jira server |
|
||||
| SSE (Legacy) | `http://localhost:3000/sse/jira` | SSE endpoint for Jira |
|
||||
|
||||
## ✅ Success Indicators
|
||||
|
||||
You'll know the configuration is working when you see:
|
||||
|
||||
1. **✅ Green status indicator** next to the server name
|
||||
2. **"Connected" status** in the server details
|
||||
3. **Tool count showing** (e.g., "15 tools available")
|
||||
4. **No error messages** in the logs
|
||||
5. **Server responds** to health check requests
|
||||
|
||||
## ❌ Common Error Indicators
|
||||
|
||||
### Connection Failed
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ ❌ jira │
|
||||
│ Status: Disconnected │
|
||||
│ Error: Failed to start server │
|
||||
│ Last error: 401 Unauthorized │
|
||||
│ │
|
||||
│ Possible causes: │
|
||||
│ • Invalid API token │
|
||||
│ • Wrong username/email │
|
||||
│ • Incorrect Jira URL │
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### UVX Not Found
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ ❌ jira │
|
||||
│ Status: Error │
|
||||
│ Error: Command not found: uvx │
|
||||
│ │
|
||||
│ Solution: Install UV │
|
||||
│ curl -LsSf https://astral.sh/uv/install.sh | sh │
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Environment Variable Not Set
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ ⚠️ jira │
|
||||
│ Status: Configuration Error │
|
||||
│ Error: Environment variable JIRA_TOKEN not found │
|
||||
│ │
|
||||
│ Solution: Check your .env file │
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 🧪 Testing Your Configuration
|
||||
|
||||
### Test 1: Health Check
|
||||
```bash
|
||||
curl http://localhost:3000/api/health
|
||||
```
|
||||
|
||||
Expected response:
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"servers": {
|
||||
"jira": "connected"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Test 2: List Servers
|
||||
```bash
|
||||
curl http://localhost:3000/api/servers
|
||||
```
|
||||
|
||||
Expected response:
|
||||
```json
|
||||
{
|
||||
"servers": [
|
||||
{
|
||||
"name": "jira",
|
||||
"status": "connected",
|
||||
"type": "stdio",
|
||||
"toolCount": 15
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Test 3: MCP Endpoint
|
||||
```bash
|
||||
curl http://localhost:3000/mcp/jira \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"method": "tools/list",
|
||||
"params": {}
|
||||
}'
|
||||
```
|
||||
|
||||
Expected response: List of available Jira tools
|
||||
|
||||
## 📊 Log Messages Explained
|
||||
|
||||
### Successful Startup
|
||||
```
|
||||
[INFO] Loading configuration from mcp_settings.json
|
||||
[INFO] Found 1 MCP server(s) to initialize
|
||||
[INFO] Starting server: jira
|
||||
[INFO] Executing: uvx mcp-atlassian --jira-url=https://...
|
||||
[INFO] Successfully connected client for server: jira
|
||||
[INFO] Successfully listed 15 tools for server: jira
|
||||
[INFO] ✅ Server jira is ready
|
||||
```
|
||||
|
||||
### Connection Issues
|
||||
```
|
||||
[ERROR] Failed to start server: jira
|
||||
[ERROR] Error: spawn uvx ENOENT
|
||||
[WARN] Server jira will retry in 5 seconds
|
||||
```
|
||||
|
||||
### Authentication Issues
|
||||
```
|
||||
[ERROR] Failed to connect to Jira
|
||||
[ERROR] HTTP 401: Unauthorized
|
||||
[ERROR] Please check your API token and credentials
|
||||
```
|
||||
|
||||
## 🔍 Debugging Steps
|
||||
|
||||
If your server shows as disconnected:
|
||||
|
||||
1. **Check logs** in the dashboard or console
|
||||
2. **Verify environment variables** are set correctly
|
||||
3. **Test manually** with uvx:
|
||||
```bash
|
||||
uvx mcp-atlassian --jira-url=https://your-company.atlassian.net --jira-username=your@email.com --jira-token=your_token
|
||||
```
|
||||
4. **Check network connectivity** to Jira
|
||||
5. **Verify API token** is still valid
|
||||
6. **Restart MCPHub** after making changes
|
||||
|
||||
## 📚 Additional Resources
|
||||
|
||||
- [Quick Start Guide](./QUICK_START_JIRA.md)
|
||||
- [Complete Setup Guide](./README_ATLASSIAN_JIRA.md)
|
||||
- [MCPHub Documentation](https://docs.mcphubx.com/)
|
||||
- [Atlassian API Tokens](https://id.atlassian.com/manage-profile/security/api-tokens)
|
||||
@@ -1,134 +0,0 @@
|
||||
# Quick Start: Jira Cloud Integration
|
||||
|
||||
This is a quick 5-minute setup guide for connecting MCPHub to Jira Cloud.
|
||||
|
||||
## ⚡ Quick Setup (5 minutes)
|
||||
|
||||
### Step 1: Get Your Jira API Token (2 minutes)
|
||||
|
||||
1. Go to https://id.atlassian.com/manage-profile/security/api-tokens
|
||||
2. Click **"Create API token"**
|
||||
3. Label it "MCPHub Integration"
|
||||
4. **Copy the token** (you can't see it again!)
|
||||
|
||||
### Step 2: Find Your Jira URL (30 seconds)
|
||||
|
||||
Your Jira URL is what you see in your browser:
|
||||
- Example: `https://mycompany.atlassian.net`
|
||||
- ✅ Include: `https://` protocol
|
||||
- ❌ Don't include: trailing `/` or `/jira`
|
||||
|
||||
### Step 3: Create .env File (1 minute)
|
||||
|
||||
Create a `.env` file in your MCPHub root directory:
|
||||
|
||||
```bash
|
||||
JIRA_URL=https://mycompany.atlassian.net
|
||||
JIRA_USERNAME=myemail@company.com
|
||||
JIRA_TOKEN=paste_your_token_here
|
||||
```
|
||||
|
||||
Replace with your actual values!
|
||||
|
||||
### Step 4: Update mcp_settings.json (1 minute)
|
||||
|
||||
Add this to your `mcp_settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"jira": {
|
||||
"command": "uvx",
|
||||
"args": [
|
||||
"mcp-atlassian",
|
||||
"--jira-url=${JIRA_URL}",
|
||||
"--jira-username=${JIRA_USERNAME}",
|
||||
"--jira-token=${JIRA_TOKEN}"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 5: Install UV & Start MCPHub (1 minute)
|
||||
|
||||
#### Install UV (if not already installed):
|
||||
|
||||
**macOS/Linux:**
|
||||
```bash
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
```
|
||||
|
||||
**Windows:**
|
||||
```powershell
|
||||
irm https://astral.sh/uv/install.ps1 | iex
|
||||
```
|
||||
|
||||
#### Start MCPHub:
|
||||
|
||||
**With Docker:**
|
||||
```bash
|
||||
docker run -p 3000:3000 \
|
||||
--env-file .env \
|
||||
-v ./mcp_settings.json:/app/mcp_settings.json \
|
||||
samanhappy/mcphub
|
||||
```
|
||||
|
||||
**Without Docker:**
|
||||
```bash
|
||||
pnpm install
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
### Step 6: Verify Connection (30 seconds)
|
||||
|
||||
1. Open http://localhost:3000
|
||||
2. Login with default credentials (see [README_ATLASSIAN_JIRA.md](./README_ATLASSIAN_JIRA.md#verification) for credentials)
|
||||
|
||||
**⚠️ CRITICAL:** Immediately change the admin password through dashboard Settings → Users
|
||||
|
||||
3. Check dashboard - you should see "jira" server as "Connected" ✅
|
||||
|
||||
## 🎉 That's It!
|
||||
|
||||
You can now use Jira through MCPHub at:
|
||||
- All servers: `http://localhost:3000/mcp`
|
||||
- Jira only: `http://localhost:3000/mcp/jira`
|
||||
|
||||
## 🐛 Common Issues
|
||||
|
||||
### "uvx command not found"
|
||||
```bash
|
||||
# Install UV first (see Step 5)
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
```
|
||||
|
||||
### "401 Unauthorized"
|
||||
- Double-check your API token
|
||||
- Make sure username is your email
|
||||
- Try regenerating the API token
|
||||
|
||||
### Server shows "Disconnected"
|
||||
- Check logs for specific errors
|
||||
- Verify .env file is in the correct location
|
||||
- Ensure no trailing slashes in JIRA_URL
|
||||
|
||||
### "Downloading cryptography" errors
|
||||
- This is usually temporary
|
||||
- Wait and restart MCPHub
|
||||
- Check internet connection
|
||||
|
||||
## 📚 Need More Help?
|
||||
|
||||
See [README_ATLASSIAN_JIRA.md](./README_ATLASSIAN_JIRA.md) for the complete guide with:
|
||||
- Both Jira + Confluence setup
|
||||
- Detailed troubleshooting
|
||||
- Security best practices
|
||||
- Example use cases
|
||||
|
||||
## 🔒 Security Reminder
|
||||
|
||||
- ✅ Never commit `.env` to git
|
||||
- ✅ Keep API tokens secret
|
||||
- ✅ Rotate tokens regularly
|
||||
- ✅ Use different tokens for dev/prod
|
||||
@@ -1,233 +0,0 @@
|
||||
# MCPHub Configuration Examples
|
||||
|
||||
This directory contains example configurations for various MCP servers and use cases.
|
||||
|
||||
## 📁 Directory Contents
|
||||
|
||||
### Atlassian/Jira Configuration
|
||||
|
||||
| File | Description | Best For |
|
||||
|------|-------------|----------|
|
||||
| [QUICK_START_JIRA.md](./QUICK_START_JIRA.md) | 5-minute quick start guide | Getting started fast with Jira Cloud |
|
||||
| [README_ATLASSIAN_JIRA.md](./README_ATLASSIAN_JIRA.md) | Complete setup guide | Comprehensive setup with troubleshooting |
|
||||
| [CONFIGURATION_SCREENSHOT_GUIDE.md](./CONFIGURATION_SCREENSHOT_GUIDE.md) | Visual configuration guide | Understanding the dashboard and logs |
|
||||
| [mcp_settings_atlassian_jira.json](./mcp_settings_atlassian_jira.json) | Basic Jira configuration | Copy-paste configuration template |
|
||||
| [.env.atlassian.example](./.env.atlassian.example) | Environment variables template | Setting up credentials securely |
|
||||
|
||||
### General Configuration Examples
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| [mcp_settings_with_env_vars.json](./mcp_settings_with_env_vars.json) | Environment variable examples for various server types (SSE, HTTP, stdio, OpenAPI) |
|
||||
| [openapi-schema-config.json](./openapi-schema-config.json) | OpenAPI-based MCP server configuration examples |
|
||||
|
||||
## 🚀 Quick Start Guides
|
||||
|
||||
### For Jira Cloud Users
|
||||
|
||||
**New to MCPHub?** Start here: [QUICK_START_JIRA.md](./QUICK_START_JIRA.md)
|
||||
|
||||
This 5-minute guide covers:
|
||||
- ✅ Getting your API token
|
||||
- ✅ Basic configuration
|
||||
- ✅ Starting MCPHub
|
||||
- ✅ Verifying connection
|
||||
|
||||
### For Experienced Users
|
||||
|
||||
**Need detailed setup?** See: [README_ATLASSIAN_JIRA.md](./README_ATLASSIAN_JIRA.md)
|
||||
|
||||
This comprehensive guide includes:
|
||||
- 📋 Both Jira and Confluence configuration
|
||||
- 🔧 Multiple installation methods (uvx, python, docker)
|
||||
- 🐛 Extensive troubleshooting section
|
||||
- 🔒 Security best practices
|
||||
- 💡 Example use cases
|
||||
|
||||
### Need Visual Guidance?
|
||||
|
||||
**Want to see what to expect?** Check: [CONFIGURATION_SCREENSHOT_GUIDE.md](./CONFIGURATION_SCREENSHOT_GUIDE.md)
|
||||
|
||||
This visual guide shows:
|
||||
- 📊 Expected dashboard views
|
||||
- ✅ Success indicators
|
||||
- ❌ Common error messages
|
||||
- 🧪 Test commands and expected outputs
|
||||
|
||||
## 📝 Configuration Templates
|
||||
|
||||
### Jira Cloud Only
|
||||
|
||||
Minimal configuration for Jira Cloud:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"jira": {
|
||||
"command": "uvx",
|
||||
"args": [
|
||||
"mcp-atlassian",
|
||||
"--jira-url=${JIRA_URL}",
|
||||
"--jira-username=${JIRA_USERNAME}",
|
||||
"--jira-token=${JIRA_TOKEN}"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Jira + Confluence
|
||||
|
||||
Combined configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"atlassian": {
|
||||
"command": "uvx",
|
||||
"args": [
|
||||
"mcp-atlassian",
|
||||
"--jira-url=${JIRA_URL}",
|
||||
"--jira-username=${JIRA_USERNAME}",
|
||||
"--jira-token=${JIRA_TOKEN}",
|
||||
"--confluence-url=${CONFLUENCE_URL}",
|
||||
"--confluence-username=${CONFLUENCE_USERNAME}",
|
||||
"--confluence-token=${CONFLUENCE_TOKEN}"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Create a `.env` file based on [.env.atlassian.example](./.env.atlassian.example):
|
||||
|
||||
```env
|
||||
JIRA_URL=https://your-company.atlassian.net
|
||||
JIRA_USERNAME=your.email@company.com
|
||||
JIRA_TOKEN=your_api_token_here
|
||||
```
|
||||
|
||||
## 🔐 Security Best Practices
|
||||
|
||||
1. **Never commit sensitive data**
|
||||
- ✅ Use `.env` files for credentials
|
||||
- ✅ Add `.env` to `.gitignore`
|
||||
- ✅ Use environment variable substitution: `${VAR_NAME}`
|
||||
|
||||
2. **Protect your API tokens**
|
||||
- ✅ Rotate tokens regularly
|
||||
- ✅ Use different tokens for dev/staging/prod
|
||||
- ✅ Revoke unused tokens immediately
|
||||
|
||||
3. **Secure your configuration**
|
||||
- ✅ Restrict file permissions on `.env` files
|
||||
- ✅ Use secrets management in production
|
||||
- ✅ Audit token usage regularly
|
||||
|
||||
## 🛠️ Common Use Cases
|
||||
|
||||
### Case 1: Development Environment
|
||||
|
||||
**Scenario**: Testing Jira integration locally
|
||||
|
||||
**Files needed**:
|
||||
- `mcp_settings_atlassian_jira.json` → Copy to `mcp_settings.json`
|
||||
- `.env.atlassian.example` → Copy to `.env` and fill in values
|
||||
|
||||
**Steps**:
|
||||
1. Copy template files
|
||||
2. Fill in your credentials
|
||||
3. Run `pnpm dev`
|
||||
|
||||
### Case 2: Production Deployment
|
||||
|
||||
**Scenario**: Deploying MCPHub with Jira to production
|
||||
|
||||
**Approach**:
|
||||
- Use environment variables in configuration
|
||||
- Store secrets in your deployment platform's secrets manager
|
||||
- Use Docker with environment file: `docker run --env-file .env ...`
|
||||
|
||||
### Case 3: Multiple Environments
|
||||
|
||||
**Scenario**: Separate dev, staging, prod configurations
|
||||
|
||||
**Structure**:
|
||||
```
|
||||
.env.development
|
||||
.env.staging
|
||||
.env.production
|
||||
```
|
||||
|
||||
**Usage**:
|
||||
```bash
|
||||
# Development
|
||||
docker run --env-file .env.development ...
|
||||
|
||||
# Staging
|
||||
docker run --env-file .env.staging ...
|
||||
|
||||
# Production
|
||||
docker run --env-file .env.production ...
|
||||
```
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### Quick Diagnostics
|
||||
|
||||
| Symptom | Likely Cause | Quick Fix |
|
||||
|---------|--------------|-----------|
|
||||
| "uvx command not found" | UV not installed | Install UV: `curl -LsSf https://astral.sh/uv/install.sh | sh` |
|
||||
| "401 Unauthorized" | Wrong API token | Regenerate token at Atlassian settings |
|
||||
| Server "Disconnected" | Missing env vars | Check `.env` file exists and has values |
|
||||
| "Downloading cryptography" errors | Network/Python issue | Wait and retry, check internet connection |
|
||||
|
||||
### Detailed Troubleshooting
|
||||
|
||||
For comprehensive troubleshooting steps, see:
|
||||
- [README_ATLASSIAN_JIRA.md - Troubleshooting Section](./README_ATLASSIAN_JIRA.md#troubleshooting)
|
||||
- [CONFIGURATION_SCREENSHOT_GUIDE.md - Error Indicators](./CONFIGURATION_SCREENSHOT_GUIDE.md#-common-error-indicators)
|
||||
|
||||
## 📚 Additional Resources
|
||||
|
||||
### Official Documentation
|
||||
|
||||
- [MCPHub Documentation](https://docs.mcphubx.com/)
|
||||
- [MCPHub GitHub Repository](https://github.com/samanhappy/mcphub)
|
||||
- [MCP Protocol Specification](https://modelcontextprotocol.io/)
|
||||
|
||||
### Atlassian Resources
|
||||
|
||||
- [Atlassian API Tokens](https://id.atlassian.com/manage-profile/security/api-tokens)
|
||||
- [Jira Cloud REST API](https://developer.atlassian.com/cloud/jira/platform/rest/v3/)
|
||||
- [Confluence Cloud REST API](https://developer.atlassian.com/cloud/confluence/rest/v2/)
|
||||
- [MCP Atlassian Server](https://github.com/sooperset/mcp-atlassian)
|
||||
|
||||
### Community Support
|
||||
|
||||
- [MCPHub Discord Community](https://discord.gg/qMKNsn5Q)
|
||||
- [GitHub Issues](https://github.com/samanhappy/mcphub/issues)
|
||||
- [GitHub Discussions](https://github.com/samanhappy/mcphub/discussions)
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
Have a useful configuration example? We'd love to include it!
|
||||
|
||||
1. Create your example configuration
|
||||
2. Add documentation explaining the setup
|
||||
3. Submit a pull request to the repository
|
||||
|
||||
Example contributions:
|
||||
- Configuration for other MCP servers
|
||||
- Multi-server setup examples
|
||||
- Docker Compose configurations
|
||||
- Kubernetes deployment examples
|
||||
- CI/CD integration examples
|
||||
|
||||
## 📄 License
|
||||
|
||||
All examples in this directory are provided under the same license as MCPHub (Apache 2.0).
|
||||
|
||||
Feel free to use, modify, and distribute these examples as needed for your projects.
|
||||
@@ -1,319 +0,0 @@
|
||||
# Atlassian Jira Cloud MCP Server Configuration
|
||||
|
||||
This guide provides detailed instructions for configuring the MCP Atlassian server to connect to Jira Cloud in MCPHub.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **Jira Cloud Account**: You need access to a Jira Cloud instance
|
||||
2. **API Token**: Generate an API token from your Atlassian account
|
||||
3. **Python/UV**: The mcp-atlassian server requires Python and `uvx` (UV package manager)
|
||||
|
||||
## Step 1: Generate Jira API Token
|
||||
|
||||
1. Go to [Atlassian Account Settings](https://id.atlassian.com/manage-profile/security/api-tokens)
|
||||
2. Click **"Create API token"**
|
||||
3. Give it a label (e.g., "MCPHub Integration")
|
||||
4. Copy the generated token (you won't be able to see it again!)
|
||||
5. Save it securely
|
||||
|
||||
## Step 2: Get Your Jira Information
|
||||
|
||||
You'll need the following information:
|
||||
|
||||
- **JIRA_URL**: Your Jira Cloud URL (e.g., `https://your-company.atlassian.net`)
|
||||
- **JIRA_USERNAME**: Your Atlassian account email (e.g., `your.email@company.com`)
|
||||
- **JIRA_TOKEN**: The API token you generated in Step 1
|
||||
|
||||
## Step 3: Set Environment Variables
|
||||
|
||||
Create or update your `.env` file in the MCPHub root directory:
|
||||
|
||||
```bash
|
||||
# Jira Configuration
|
||||
JIRA_URL=https://your-company.atlassian.net
|
||||
JIRA_USERNAME=your.email@company.com
|
||||
JIRA_TOKEN=your_api_token_here
|
||||
```
|
||||
|
||||
**Important Security Note**: Never commit your `.env` file to version control. It should be listed in `.gitignore`.
|
||||
|
||||
## Step 4: Configure MCPHub
|
||||
|
||||
### Option 1: Using Environment Variables (Recommended)
|
||||
|
||||
Update your `mcp_settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"atlassian": {
|
||||
"command": "uvx",
|
||||
"args": [
|
||||
"mcp-atlassian",
|
||||
"--jira-url=${JIRA_URL}",
|
||||
"--jira-username=${JIRA_USERNAME}",
|
||||
"--jira-token=${JIRA_TOKEN}"
|
||||
],
|
||||
"env": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Option 2: Direct Configuration (Not Recommended)
|
||||
|
||||
If you prefer not to use environment variables (less secure):
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"atlassian": {
|
||||
"command": "uvx",
|
||||
"args": [
|
||||
"mcp-atlassian",
|
||||
"--jira-url=https://your-company.atlassian.net",
|
||||
"--jira-username=your.email@company.com",
|
||||
"--jira-token=your_api_token_here"
|
||||
],
|
||||
"env": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Option 3: Jira Only (Without Confluence)
|
||||
|
||||
If you only want to use Jira and not Confluence:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"jira": {
|
||||
"command": "uvx",
|
||||
"args": [
|
||||
"mcp-atlassian",
|
||||
"--jira-url=${JIRA_URL}",
|
||||
"--jira-username=${JIRA_USERNAME}",
|
||||
"--jira-token=${JIRA_TOKEN}"
|
||||
],
|
||||
"env": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Option 4: Both Jira and Confluence
|
||||
|
||||
To use both Jira and Confluence, you'll need API tokens for both:
|
||||
|
||||
```bash
|
||||
# .env file
|
||||
JIRA_URL=https://your-company.atlassian.net
|
||||
JIRA_USERNAME=your.email@company.com
|
||||
JIRA_TOKEN=your_jira_api_token
|
||||
|
||||
CONFLUENCE_URL=https://your-company.atlassian.net/wiki
|
||||
CONFLUENCE_USERNAME=your.email@company.com
|
||||
CONFLUENCE_TOKEN=your_confluence_api_token
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"atlassian": {
|
||||
"command": "uvx",
|
||||
"args": [
|
||||
"mcp-atlassian",
|
||||
"--confluence-url=${CONFLUENCE_URL}",
|
||||
"--confluence-username=${CONFLUENCE_USERNAME}",
|
||||
"--confluence-token=${CONFLUENCE_TOKEN}",
|
||||
"--jira-url=${JIRA_URL}",
|
||||
"--jira-username=${JIRA_USERNAME}",
|
||||
"--jira-token=${JIRA_TOKEN}"
|
||||
],
|
||||
"env": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Note**: For Atlassian Cloud, you can often use the same API token for both Jira and Confluence.
|
||||
|
||||
## Step 5: Install UV (if not already installed)
|
||||
|
||||
The mcp-atlassian server uses `uvx` to run. Install UV if you haven't already:
|
||||
|
||||
### On macOS/Linux:
|
||||
```bash
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
```
|
||||
|
||||
### On Windows:
|
||||
```powershell
|
||||
powershell -c "irm https://astral.sh/uv/install.ps1 | iex"
|
||||
```
|
||||
|
||||
### Using pip:
|
||||
```bash
|
||||
pip install uv
|
||||
```
|
||||
|
||||
## Step 6: Start MCPHub
|
||||
|
||||
### Using Docker:
|
||||
```bash
|
||||
docker run -p 3000:3000 \
|
||||
-v ./mcp_settings.json:/app/mcp_settings.json \
|
||||
-v ./data:/app/data \
|
||||
-e JIRA_URL="${JIRA_URL}" \
|
||||
-e JIRA_USERNAME="${JIRA_USERNAME}" \
|
||||
-e JIRA_TOKEN="${JIRA_TOKEN}" \
|
||||
samanhappy/mcphub
|
||||
```
|
||||
|
||||
### Using Development Mode:
|
||||
```bash
|
||||
pnpm install
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
### Using Production Mode:
|
||||
```bash
|
||||
pnpm install
|
||||
pnpm build
|
||||
pnpm start
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
After starting MCPHub:
|
||||
|
||||
1. Open `http://localhost:3000` in your browser
|
||||
2. Log in with default credentials: `admin` / `admin123`
|
||||
|
||||
**⚠️ SECURITY WARNING:** Change the default admin password immediately in production!
|
||||
|
||||
**To change the password:**
|
||||
- Option 1: Use the dashboard after logging in (Settings → Users → Change Password)
|
||||
- Option 2: Generate a bcrypt hash and update `mcp_settings.json`:
|
||||
```bash
|
||||
node -e "console.log(require('bcrypt').hashSync('your-new-password', 10))"
|
||||
```
|
||||
|
||||
3. Check the dashboard to see if the Atlassian server is connected
|
||||
4. Look for the server status - it should show as "Connected" or "Running"
|
||||
5. Check the logs for any connection errors
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Error: "uvx command not found"
|
||||
|
||||
**Solution**: Install UV as described in Step 5 above.
|
||||
|
||||
### Error: "Traceback (most recent call last): File ... mcp-atlassian"
|
||||
|
||||
This error usually indicates:
|
||||
1. Missing or incorrect API credentials
|
||||
2. Network connectivity issues
|
||||
3. Python dependency issues
|
||||
|
||||
**Solutions**:
|
||||
- Verify your API token is correct
|
||||
- Ensure your Jira URL doesn't have trailing slashes
|
||||
- Check that your username is the email address you use for Atlassian
|
||||
- Verify network connectivity to your Jira instance
|
||||
- Try regenerating your API token
|
||||
|
||||
### Error: "401 Unauthorized"
|
||||
|
||||
**Solution**:
|
||||
- Double-check your API token is correct
|
||||
- Ensure you're using the email address associated with your Atlassian account
|
||||
- Regenerate your API token if needed
|
||||
|
||||
### Error: "403 Forbidden"
|
||||
|
||||
**Solution**:
|
||||
- Check that your account has appropriate permissions in Jira
|
||||
- Verify your Jira administrator hasn't restricted API access
|
||||
|
||||
### Error: Downloading cryptography errors
|
||||
|
||||
**Solution**:
|
||||
- This is usually a transient network or Python package installation issue
|
||||
- Wait a moment and try restarting MCPHub
|
||||
- Ensure you have a stable internet connection
|
||||
- If the issue persists, try installing mcp-atlassian manually:
|
||||
```bash
|
||||
uvx mcp-atlassian --help
|
||||
```
|
||||
|
||||
### Server shows as "Disconnected"
|
||||
|
||||
**Solution**:
|
||||
1. Check MCPHub logs for specific error messages
|
||||
2. Verify all environment variables are set correctly
|
||||
3. Test the connection manually:
|
||||
```bash
|
||||
uvx mcp-atlassian \
|
||||
--jira-url=https://your-company.atlassian.net \
|
||||
--jira-username=your.email@company.com \
|
||||
--jira-token=your_token
|
||||
```
|
||||
|
||||
## Using the Jira MCP Server
|
||||
|
||||
Once connected, you can use the Jira MCP server to:
|
||||
|
||||
- **Search Issues**: Query Jira issues using JQL
|
||||
- **Read Issues**: Get detailed information about specific issues
|
||||
- **Access Projects**: List and retrieve project metadata
|
||||
- **View Comments**: Read issue comments and discussions
|
||||
- **Get Transitions**: Check available status transitions for issues
|
||||
|
||||
Access the server through:
|
||||
- **All servers**: `http://localhost:3000/mcp`
|
||||
- **Specific server**: `http://localhost:3000/mcp/atlassian`
|
||||
- **Server groups**: `http://localhost:3000/mcp/{group}` (if configured)
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [MCP Atlassian GitHub Repository](https://github.com/sooperset/mcp-atlassian)
|
||||
- [Atlassian API Token Documentation](https://support.atlassian.com/atlassian-account/docs/manage-api-tokens-for-your-atlassian-account/)
|
||||
- [Jira Cloud REST API](https://developer.atlassian.com/cloud/jira/platform/rest/v3/)
|
||||
- [MCPHub Documentation](https://docs.mcphubx.com/)
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
1. ✅ **Always use environment variables** for sensitive credentials
|
||||
2. ✅ **Never commit `.env` files** to version control
|
||||
3. ✅ **Rotate API tokens** regularly
|
||||
4. ✅ **Use separate tokens** for different environments (dev, staging, prod)
|
||||
5. ✅ **Restrict API token permissions** to only what's needed
|
||||
6. ✅ **Monitor token usage** in Atlassian account settings
|
||||
7. ✅ **Revoke unused tokens** immediately
|
||||
|
||||
## Example Use Cases
|
||||
|
||||
### Example 1: Search for Issues
|
||||
Query: "List all open bugs assigned to me"
|
||||
- Tool: `jira_search_issues`
|
||||
- JQL: `project = MYPROJECT AND status = Open AND assignee = currentUser() AND type = Bug`
|
||||
|
||||
### Example 2: Get Issue Details
|
||||
Query: "Show me details of issue PROJ-123"
|
||||
- Tool: `jira_get_issue`
|
||||
- Issue Key: `PROJ-123`
|
||||
|
||||
### Example 3: List Projects
|
||||
Query: "What Jira projects do I have access to?"
|
||||
- Tool: `jira_list_projects`
|
||||
|
||||
## Need Help?
|
||||
|
||||
If you're still experiencing issues:
|
||||
|
||||
1. Check the [MCPHub Discord community](https://discord.gg/qMKNsn5Q)
|
||||
2. Review [MCPHub GitHub Issues](https://github.com/samanhappy/mcphub/issues)
|
||||
3. Check [mcp-atlassian Issues](https://github.com/sooperset/mcp-atlassian/issues)
|
||||
4. Contact your Jira administrator for API access questions
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"atlassian": {
|
||||
"command": "uvx",
|
||||
"args": [
|
||||
"mcp-atlassian",
|
||||
"--jira-url=${JIRA_URL}",
|
||||
"--jira-username=${JIRA_USERNAME}",
|
||||
"--jira-token=${JIRA_TOKEN}"
|
||||
],
|
||||
"env": {}
|
||||
}
|
||||
},
|
||||
"users": [
|
||||
{
|
||||
"username": "admin",
|
||||
"_comment": "Password must be a bcrypt hash. Generate with: node -e \"console.log(require('bcrypt').hashSync('your-password', 10))\"",
|
||||
"password": "${ADMIN_PASSWORD_HASH}",
|
||||
"isAdmin": true
|
||||
}
|
||||
]
|
||||
}
|
||||
61
examples/mcp_settings_with_connection_modes.json
Normal file
61
examples/mcp_settings_with_connection_modes.json
Normal file
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft-07/schema",
|
||||
"description": "Example MCP settings showing different connection modes",
|
||||
"mcpServers": {
|
||||
"github": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-github"],
|
||||
"connectionMode": "persistent",
|
||||
"env": {
|
||||
"GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_TOKEN}"
|
||||
},
|
||||
"enabled": true
|
||||
},
|
||||
"playwright": {
|
||||
"command": "npx",
|
||||
"args": ["@playwright/mcp@latest", "--headless"],
|
||||
"connectionMode": "persistent",
|
||||
"enabled": true
|
||||
},
|
||||
"pdf-reader": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "pdf-mcp-server"],
|
||||
"connectionMode": "on-demand",
|
||||
"env": {
|
||||
"PDF_CACHE_DIR": "/tmp/pdf-cache"
|
||||
},
|
||||
"enabled": true
|
||||
},
|
||||
"image-processor": {
|
||||
"command": "python",
|
||||
"args": ["-m", "image_mcp_server"],
|
||||
"connectionMode": "on-demand",
|
||||
"env": {
|
||||
"IMAGE_OUTPUT_DIR": "/tmp/images"
|
||||
},
|
||||
"enabled": true
|
||||
},
|
||||
"fetch": {
|
||||
"command": "uvx",
|
||||
"args": ["mcp-server-fetch"],
|
||||
"enabled": true
|
||||
},
|
||||
"slack": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-slack"],
|
||||
"connectionMode": "persistent",
|
||||
"env": {
|
||||
"SLACK_BOT_TOKEN": "${SLACK_BOT_TOKEN}",
|
||||
"SLACK_TEAM_ID": "${SLACK_TEAM_ID}"
|
||||
},
|
||||
"enabled": true
|
||||
}
|
||||
},
|
||||
"users": [
|
||||
{
|
||||
"username": "admin",
|
||||
"password": "$2b$10$Vt7krIvjNgyN67LXqly0uOcTpN0LI55cYRbcKC71pUDAP0nJ7RPa.",
|
||||
"isAdmin": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -369,6 +369,118 @@ export const createTransportFromConfig = async (name: string, conf: ServerConfig
|
||||
return transport;
|
||||
};
|
||||
|
||||
// Helper function to connect an on-demand server temporarily
|
||||
const connectOnDemandServer = async (serverInfo: ServerInfo): Promise<void> => {
|
||||
if (!serverInfo.config) {
|
||||
throw new Error(`Server configuration not found for on-demand server: ${serverInfo.name}`);
|
||||
}
|
||||
|
||||
console.log(`Connecting on-demand server: ${serverInfo.name}`);
|
||||
|
||||
// Create transport
|
||||
const transport = await createTransportFromConfig(serverInfo.name, serverInfo.config);
|
||||
|
||||
// Create client
|
||||
const client = new Client(
|
||||
{
|
||||
name: `mcp-client-${serverInfo.name}`,
|
||||
version: '1.0.0',
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
prompts: {},
|
||||
resources: {},
|
||||
tools: {},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// Get request options from server configuration
|
||||
const serverRequestOptions = serverInfo.config.options || {};
|
||||
const requestOptions = {
|
||||
timeout: serverRequestOptions.timeout || 60000,
|
||||
resetTimeoutOnProgress: serverRequestOptions.resetTimeoutOnProgress || false,
|
||||
maxTotalTimeout: serverRequestOptions.maxTotalTimeout,
|
||||
};
|
||||
|
||||
// Connect the client
|
||||
await client.connect(transport, requestOptions);
|
||||
|
||||
// Update server info with client and transport
|
||||
serverInfo.client = client;
|
||||
serverInfo.transport = transport;
|
||||
serverInfo.options = requestOptions;
|
||||
serverInfo.status = 'connected';
|
||||
|
||||
console.log(`Successfully connected on-demand server: ${serverInfo.name}`);
|
||||
|
||||
// List tools if not already loaded
|
||||
if (serverInfo.tools.length === 0) {
|
||||
const capabilities = client.getServerCapabilities();
|
||||
if (capabilities?.tools) {
|
||||
try {
|
||||
const tools = await client.listTools({}, requestOptions);
|
||||
serverInfo.tools = tools.tools.map((tool) => ({
|
||||
name: `${serverInfo.name}${getNameSeparator()}${tool.name}`,
|
||||
description: tool.description || '',
|
||||
inputSchema: cleanInputSchema(tool.inputSchema || {}),
|
||||
}));
|
||||
// Save tools as vector embeddings for search
|
||||
saveToolsAsVectorEmbeddings(serverInfo.name, serverInfo.tools);
|
||||
console.log(`Loaded ${serverInfo.tools.length} tools for on-demand server: ${serverInfo.name}`);
|
||||
} catch (error) {
|
||||
console.warn(`Failed to list tools for on-demand server ${serverInfo.name}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// List prompts if available
|
||||
if (capabilities?.prompts) {
|
||||
try {
|
||||
const prompts = await client.listPrompts({}, requestOptions);
|
||||
serverInfo.prompts = prompts.prompts.map((prompt) => ({
|
||||
name: `${serverInfo.name}${getNameSeparator()}${prompt.name}`,
|
||||
title: prompt.title,
|
||||
description: prompt.description,
|
||||
arguments: prompt.arguments,
|
||||
}));
|
||||
console.log(`Loaded ${serverInfo.prompts.length} prompts for on-demand server: ${serverInfo.name}`);
|
||||
} catch (error) {
|
||||
console.warn(`Failed to list prompts for on-demand server ${serverInfo.name}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to disconnect an on-demand server
|
||||
const disconnectOnDemandServer = (serverInfo: ServerInfo): void => {
|
||||
if (serverInfo.connectionMode !== 'on-demand') {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Disconnecting on-demand server: ${serverInfo.name}`);
|
||||
|
||||
try {
|
||||
if (serverInfo.client) {
|
||||
serverInfo.client.close();
|
||||
serverInfo.client = undefined;
|
||||
}
|
||||
if (serverInfo.transport) {
|
||||
serverInfo.transport.close();
|
||||
serverInfo.transport = undefined;
|
||||
}
|
||||
serverInfo.status = 'disconnected';
|
||||
console.log(`Successfully disconnected on-demand server: ${serverInfo.name}`);
|
||||
} catch (error) {
|
||||
// Log disconnect errors but don't throw - this is cleanup code that shouldn't fail the request
|
||||
// The connection is likely already closed if we get an error here
|
||||
console.warn(`Error disconnecting on-demand server ${serverInfo.name}:`, error);
|
||||
// Force status to disconnected even if cleanup had errors
|
||||
serverInfo.status = 'disconnected';
|
||||
serverInfo.client = undefined;
|
||||
serverInfo.transport = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to handle client.callTool with reconnection logic
|
||||
const callToolWithReconnect = async (
|
||||
serverInfo: ServerInfo,
|
||||
@@ -529,7 +641,6 @@ export const initializeClientsFromSettings = async (
|
||||
continue;
|
||||
}
|
||||
|
||||
let transport;
|
||||
let openApiClient;
|
||||
if (expandedConf.type === 'openapi') {
|
||||
// Handle OpenAPI type servers
|
||||
@@ -600,10 +711,43 @@ export const initializeClientsFromSettings = async (
|
||||
serverInfo.error = `Failed to initialize OpenAPI server: ${error}`;
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
transport = await createTransportFromConfig(name, expandedConf);
|
||||
}
|
||||
|
||||
// Handle on-demand connection mode servers
|
||||
// These servers connect briefly to get tools list, then disconnect
|
||||
const connectionMode = expandedConf.connectionMode || 'persistent';
|
||||
if (connectionMode === 'on-demand') {
|
||||
console.log(`Initializing on-demand server: ${name}`);
|
||||
const serverInfo: ServerInfo = {
|
||||
name,
|
||||
owner: expandedConf.owner,
|
||||
status: 'disconnected',
|
||||
error: null,
|
||||
tools: [],
|
||||
prompts: [],
|
||||
createTime: Date.now(),
|
||||
enabled: expandedConf.enabled === undefined ? true : expandedConf.enabled,
|
||||
connectionMode: 'on-demand',
|
||||
config: expandedConf,
|
||||
};
|
||||
nextServerInfos.push(serverInfo);
|
||||
|
||||
// Connect briefly to get tools list, then disconnect
|
||||
try {
|
||||
await connectOnDemandServer(serverInfo);
|
||||
console.log(`Successfully initialized on-demand server: ${name} with ${serverInfo.tools.length} tools`);
|
||||
// Disconnect immediately after getting tools
|
||||
disconnectOnDemandServer(serverInfo);
|
||||
} catch (error) {
|
||||
console.error(`Failed to initialize on-demand server ${name}:`, error);
|
||||
serverInfo.error = `Failed to initialize: ${error}`;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create transport for persistent connection mode servers (not OpenAPI, already handled above)
|
||||
const transport = await createTransportFromConfig(name, expandedConf);
|
||||
|
||||
const client = new Client(
|
||||
{
|
||||
name: `mcp-client-${name}`,
|
||||
@@ -644,6 +788,7 @@ export const initializeClientsFromSettings = async (
|
||||
transport,
|
||||
options: requestOptions,
|
||||
createTime: Date.now(),
|
||||
connectionMode: connectionMode,
|
||||
config: expandedConf, // Store reference to expanded config
|
||||
};
|
||||
|
||||
@@ -1011,8 +1156,11 @@ export const handleListToolsRequest = async (_: any, extra: any) => {
|
||||
const targetGroup = group?.startsWith('$smart/') ? group.substring(7) : undefined;
|
||||
|
||||
// Get info about available servers, filtered by target group if specified
|
||||
// Include both connected persistent servers and on-demand servers (even if disconnected)
|
||||
let availableServers = serverInfos.filter(
|
||||
(server) => server.status === 'connected' && server.enabled !== false,
|
||||
(server) =>
|
||||
server.enabled !== false &&
|
||||
(server.status === 'connected' || server.connectionMode === 'on-demand'),
|
||||
);
|
||||
|
||||
// If a target group is specified, filter servers to only those in the group
|
||||
@@ -1139,6 +1287,10 @@ Available servers: ${serversList}`,
|
||||
export const handleCallToolRequest = async (request: any, extra: any) => {
|
||||
console.log(`Handling CallToolRequest for tool: ${JSON.stringify(request.params)}`);
|
||||
try {
|
||||
// Note: On-demand server connection and disconnection are handled in the specific
|
||||
// code paths below (call_tool and regular tool handling) with try-finally blocks.
|
||||
// This outer try-catch only handles errors from operations that don't connect servers.
|
||||
|
||||
// Special handling for agent group tools
|
||||
if (request.params.name === 'search_tools') {
|
||||
const { query, limit = 10 } = request.params.arguments || {};
|
||||
@@ -1284,10 +1436,11 @@ export const handleCallToolRequest = async (request: any, extra: any) => {
|
||||
targetServerInfo = getServerByName(extra.server);
|
||||
} else {
|
||||
// Find the first server that has this tool
|
||||
// Include both connected servers and on-demand servers (even if disconnected)
|
||||
targetServerInfo = serverInfos.find(
|
||||
(serverInfo) =>
|
||||
serverInfo.status === 'connected' &&
|
||||
serverInfo.enabled !== false &&
|
||||
(serverInfo.status === 'connected' || serverInfo.connectionMode === 'on-demand') &&
|
||||
serverInfo.tools.some((tool) => tool.name === toolName),
|
||||
);
|
||||
}
|
||||
@@ -1363,6 +1516,11 @@ export const handleCallToolRequest = async (request: any, extra: any) => {
|
||||
}
|
||||
|
||||
// Call the tool on the target server (MCP servers)
|
||||
// Connect on-demand server if needed
|
||||
if (targetServerInfo.connectionMode === 'on-demand' && !targetServerInfo.client) {
|
||||
await connectOnDemandServer(targetServerInfo);
|
||||
}
|
||||
|
||||
const client = targetServerInfo.client;
|
||||
if (!client) {
|
||||
throw new Error(`Client not found for server: ${targetServerInfo.name}`);
|
||||
@@ -1379,17 +1537,23 @@ export const handleCallToolRequest = async (request: any, extra: any) => {
|
||||
const separator = getNameSeparator();
|
||||
const prefix = `${targetServerInfo.name}${separator}`;
|
||||
toolName = toolName.startsWith(prefix) ? toolName.substring(prefix.length) : toolName;
|
||||
const result = await callToolWithReconnect(
|
||||
targetServerInfo,
|
||||
{
|
||||
name: toolName,
|
||||
arguments: finalArgs,
|
||||
},
|
||||
targetServerInfo.options || {},
|
||||
);
|
||||
|
||||
try {
|
||||
const result = await callToolWithReconnect(
|
||||
targetServerInfo,
|
||||
{
|
||||
name: toolName,
|
||||
arguments: finalArgs,
|
||||
},
|
||||
targetServerInfo.options || {},
|
||||
);
|
||||
|
||||
console.log(`Tool invocation result: ${JSON.stringify(result)}`);
|
||||
return result;
|
||||
console.log(`Tool invocation result: ${JSON.stringify(result)}`);
|
||||
return result;
|
||||
} finally {
|
||||
// Disconnect on-demand server after tool call
|
||||
disconnectOnDemandServer(targetServerInfo);
|
||||
}
|
||||
}
|
||||
|
||||
// Regular tool handling
|
||||
@@ -1459,6 +1623,11 @@ export const handleCallToolRequest = async (request: any, extra: any) => {
|
||||
}
|
||||
|
||||
// Handle MCP servers
|
||||
// Connect on-demand server if needed
|
||||
if (serverInfo.connectionMode === 'on-demand' && !serverInfo.client) {
|
||||
await connectOnDemandServer(serverInfo);
|
||||
}
|
||||
|
||||
const client = serverInfo.client;
|
||||
if (!client) {
|
||||
throw new Error(`Client not found for server: ${serverInfo.name}`);
|
||||
@@ -1469,13 +1638,19 @@ export const handleCallToolRequest = async (request: any, extra: any) => {
|
||||
request.params.name = request.params.name.startsWith(prefix)
|
||||
? request.params.name.substring(prefix.length)
|
||||
: request.params.name;
|
||||
const result = await callToolWithReconnect(
|
||||
serverInfo,
|
||||
request.params,
|
||||
serverInfo.options || {},
|
||||
);
|
||||
console.log(`Tool call result: ${JSON.stringify(result)}`);
|
||||
return result;
|
||||
|
||||
try {
|
||||
const result = await callToolWithReconnect(
|
||||
serverInfo,
|
||||
request.params,
|
||||
serverInfo.options || {},
|
||||
);
|
||||
console.log(`Tool call result: ${JSON.stringify(result)}`);
|
||||
return result;
|
||||
} finally {
|
||||
// Disconnect on-demand server after tool call
|
||||
disconnectOnDemandServer(serverInfo);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error handling CallToolRequest: ${error}`);
|
||||
return {
|
||||
|
||||
@@ -204,6 +204,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)
|
||||
connectionMode?: 'persistent' | 'on-demand'; // Connection strategy: 'persistent' maintains long-running connections (default), 'on-demand' connects only when tools are called
|
||||
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
|
||||
@@ -312,6 +313,7 @@ export interface ServerInfo {
|
||||
options?: RequestOptions; // Options for requests
|
||||
createTime: number; // Timestamp of when the server was created
|
||||
enabled?: boolean; // Flag to indicate if the server is enabled
|
||||
connectionMode?: 'persistent' | 'on-demand'; // Connection strategy for this server
|
||||
keepAliveIntervalId?: NodeJS.Timeout; // Timer ID for keep-alive ping interval
|
||||
config?: ServerConfig; // Reference to the original server configuration for OpenAPI passthrough headers
|
||||
oauth?: {
|
||||
|
||||
340
tests/services/mcpService-on-demand.test.ts
Normal file
340
tests/services/mcpService-on-demand.test.ts
Normal file
@@ -0,0 +1,340 @@
|
||||
import { describe, it, expect, jest, beforeEach, afterEach } from '@jest/globals';
|
||||
|
||||
// Mock dependencies before importing mcpService
|
||||
jest.mock('../../src/services/oauthService.js', () => ({
|
||||
initializeAllOAuthClients: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../src/services/oauthClientRegistration.js', () => ({
|
||||
registerOAuthClient: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../src/services/mcpOAuthProvider.js', () => ({
|
||||
createOAuthProvider: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../src/services/groupService.js', () => ({
|
||||
getServersInGroup: jest.fn(),
|
||||
getServerConfigInGroup: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../src/services/sseService.js', () => ({
|
||||
getGroup: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../src/services/vectorSearchService.js', () => ({
|
||||
saveToolsAsVectorEmbeddings: jest.fn(),
|
||||
searchToolsByVector: jest.fn(() => Promise.resolve([])),
|
||||
}));
|
||||
|
||||
jest.mock('../../src/services/services.js', () => ({
|
||||
getDataService: jest.fn(() => ({
|
||||
filterData: (data: any) => data,
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('../../src/config/index.js', () => ({
|
||||
default: {
|
||||
mcpHubName: 'test-hub',
|
||||
mcpHubVersion: '1.0.0',
|
||||
initTimeout: 60000,
|
||||
},
|
||||
loadSettings: jest.fn(() => ({})),
|
||||
expandEnvVars: jest.fn((val: string) => val),
|
||||
replaceEnvVars: jest.fn((obj: any) => obj),
|
||||
getNameSeparator: jest.fn(() => '-'),
|
||||
}));
|
||||
|
||||
// Mock Client
|
||||
const mockClient = {
|
||||
connect: jest.fn(),
|
||||
close: jest.fn(),
|
||||
listTools: jest.fn(),
|
||||
listPrompts: jest.fn(),
|
||||
getServerCapabilities: jest.fn(() => ({
|
||||
tools: {},
|
||||
prompts: {},
|
||||
})),
|
||||
callTool: jest.fn(),
|
||||
};
|
||||
|
||||
jest.mock('@modelcontextprotocol/sdk/client/index.js', () => ({
|
||||
Client: jest.fn(() => mockClient),
|
||||
}));
|
||||
|
||||
// Mock StdioClientTransport
|
||||
const mockTransport = {
|
||||
close: jest.fn(),
|
||||
stderr: null,
|
||||
};
|
||||
|
||||
jest.mock('@modelcontextprotocol/sdk/client/stdio.js', () => ({
|
||||
StdioClientTransport: jest.fn(() => mockTransport),
|
||||
}));
|
||||
|
||||
// Mock DAO
|
||||
const mockServerDao = {
|
||||
findAll: jest.fn(),
|
||||
findById: jest.fn(),
|
||||
create: jest.fn(),
|
||||
update: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
exists: jest.fn(),
|
||||
setEnabled: jest.fn(),
|
||||
};
|
||||
|
||||
jest.mock('../../src/dao/index.js', () => ({
|
||||
getServerDao: jest.fn(() => mockServerDao),
|
||||
}));
|
||||
|
||||
import { initializeClientsFromSettings, handleCallToolRequest } from '../../src/services/mcpService.js';
|
||||
|
||||
describe('On-Demand MCP Server Connection Mode', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockClient.connect.mockResolvedValue(undefined);
|
||||
mockClient.close.mockReturnValue(undefined);
|
||||
mockClient.listTools.mockResolvedValue({
|
||||
tools: [
|
||||
{
|
||||
name: 'test-tool',
|
||||
description: 'Test tool',
|
||||
inputSchema: { type: 'object' },
|
||||
},
|
||||
],
|
||||
});
|
||||
mockClient.listPrompts.mockResolvedValue({
|
||||
prompts: [],
|
||||
});
|
||||
mockClient.callTool.mockResolvedValue({
|
||||
content: [{ type: 'text', text: 'Success' }],
|
||||
});
|
||||
mockTransport.close.mockReturnValue(undefined);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('Server Initialization', () => {
|
||||
it('should not maintain persistent connection for on-demand servers', async () => {
|
||||
mockServerDao.findAll.mockResolvedValue([
|
||||
{
|
||||
name: 'on-demand-server',
|
||||
command: 'node',
|
||||
args: ['test.js'],
|
||||
connectionMode: 'on-demand',
|
||||
enabled: true,
|
||||
},
|
||||
]);
|
||||
|
||||
const serverInfos = await initializeClientsFromSettings(true);
|
||||
|
||||
expect(serverInfos).toHaveLength(1);
|
||||
expect(serverInfos[0].name).toBe('on-demand-server');
|
||||
expect(serverInfos[0].connectionMode).toBe('on-demand');
|
||||
expect(serverInfos[0].status).toBe('disconnected');
|
||||
// Should connect once to get tools, then disconnect
|
||||
expect(mockClient.connect).toHaveBeenCalledTimes(1);
|
||||
expect(mockTransport.close).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should load tools during initialization for on-demand servers', async () => {
|
||||
mockServerDao.findAll.mockResolvedValue([
|
||||
{
|
||||
name: 'on-demand-server',
|
||||
command: 'node',
|
||||
args: ['test.js'],
|
||||
connectionMode: 'on-demand',
|
||||
enabled: true,
|
||||
},
|
||||
]);
|
||||
|
||||
const serverInfos = await initializeClientsFromSettings(true);
|
||||
|
||||
expect(serverInfos[0].tools).toHaveLength(1);
|
||||
expect(serverInfos[0].tools[0].name).toBe('on-demand-server-test-tool');
|
||||
expect(mockClient.listTools).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should maintain persistent connection for default connection mode', async () => {
|
||||
mockServerDao.findAll.mockResolvedValue([
|
||||
{
|
||||
name: 'persistent-server',
|
||||
command: 'node',
|
||||
args: ['test.js'],
|
||||
enabled: true,
|
||||
},
|
||||
]);
|
||||
|
||||
const serverInfos = await initializeClientsFromSettings(true);
|
||||
|
||||
expect(serverInfos).toHaveLength(1);
|
||||
expect(serverInfos[0].connectionMode).toBe('persistent');
|
||||
expect(mockClient.connect).toHaveBeenCalledTimes(1);
|
||||
// Should not disconnect immediately
|
||||
expect(mockTransport.close).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle initialization errors for on-demand servers gracefully', async () => {
|
||||
mockClient.connect.mockRejectedValueOnce(new Error('Connection failed'));
|
||||
mockServerDao.findAll.mockResolvedValue([
|
||||
{
|
||||
name: 'failing-server',
|
||||
command: 'node',
|
||||
args: ['test.js'],
|
||||
connectionMode: 'on-demand',
|
||||
enabled: true,
|
||||
},
|
||||
]);
|
||||
|
||||
const serverInfos = await initializeClientsFromSettings(true);
|
||||
|
||||
expect(serverInfos).toHaveLength(1);
|
||||
expect(serverInfos[0].status).toBe('disconnected');
|
||||
expect(serverInfos[0].error).toContain('Failed to initialize');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Tool Invocation with On-Demand Servers', () => {
|
||||
beforeEach(async () => {
|
||||
// Set up server infos with an on-demand server that's disconnected
|
||||
mockServerDao.findAll.mockResolvedValue([
|
||||
{
|
||||
name: 'on-demand-server',
|
||||
command: 'node',
|
||||
args: ['test.js'],
|
||||
connectionMode: 'on-demand',
|
||||
enabled: true,
|
||||
},
|
||||
]);
|
||||
|
||||
// Initialize to get the server set up
|
||||
await initializeClientsFromSettings(true);
|
||||
|
||||
// Clear mocks after initialization
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Reset mock implementations
|
||||
mockClient.connect.mockResolvedValue(undefined);
|
||||
mockClient.listTools.mockResolvedValue({
|
||||
tools: [
|
||||
{
|
||||
name: 'test-tool',
|
||||
description: 'Test tool',
|
||||
inputSchema: { type: 'object' },
|
||||
},
|
||||
],
|
||||
});
|
||||
mockClient.callTool.mockResolvedValue({
|
||||
content: [{ type: 'text', text: 'Success' }],
|
||||
});
|
||||
});
|
||||
|
||||
it('should connect on-demand server before tool invocation', async () => {
|
||||
const request = {
|
||||
params: {
|
||||
name: 'on-demand-server-test-tool',
|
||||
arguments: { arg1: 'value1' },
|
||||
},
|
||||
};
|
||||
|
||||
await handleCallToolRequest(request, {});
|
||||
|
||||
// Should connect before calling the tool
|
||||
expect(mockClient.connect).toHaveBeenCalledTimes(1);
|
||||
expect(mockClient.callTool).toHaveBeenCalledWith(
|
||||
{
|
||||
name: 'test-tool',
|
||||
arguments: { arg1: 'value1' },
|
||||
},
|
||||
undefined,
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
it('should disconnect on-demand server after tool invocation', async () => {
|
||||
const request = {
|
||||
params: {
|
||||
name: 'on-demand-server-test-tool',
|
||||
arguments: {},
|
||||
},
|
||||
};
|
||||
|
||||
await handleCallToolRequest(request, {});
|
||||
|
||||
// Should disconnect after calling the tool
|
||||
expect(mockTransport.close).toHaveBeenCalledTimes(1);
|
||||
expect(mockClient.close).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should disconnect on-demand server even if tool invocation fails', async () => {
|
||||
mockClient.callTool.mockRejectedValueOnce(new Error('Tool execution failed'));
|
||||
|
||||
const request = {
|
||||
params: {
|
||||
name: 'on-demand-server-test-tool',
|
||||
arguments: {},
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
await handleCallToolRequest(request, {});
|
||||
} catch (error) {
|
||||
// Expected to fail
|
||||
}
|
||||
|
||||
// Should still disconnect after error
|
||||
expect(mockTransport.close).toHaveBeenCalledTimes(1);
|
||||
expect(mockClient.close).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should return error for call_tool if server not found', async () => {
|
||||
const request = {
|
||||
params: {
|
||||
name: 'call_tool',
|
||||
arguments: {
|
||||
toolName: 'nonexistent-server-tool',
|
||||
arguments: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = await handleCallToolRequest(request, {});
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content[0].text).toContain('No available servers found');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Mixed Server Modes', () => {
|
||||
it('should handle both persistent and on-demand servers together', async () => {
|
||||
mockServerDao.findAll.mockResolvedValue([
|
||||
{
|
||||
name: 'persistent-server',
|
||||
command: 'node',
|
||||
args: ['persistent.js'],
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
name: 'on-demand-server',
|
||||
command: 'node',
|
||||
args: ['on-demand.js'],
|
||||
connectionMode: 'on-demand',
|
||||
enabled: true,
|
||||
},
|
||||
]);
|
||||
|
||||
const serverInfos = await initializeClientsFromSettings(true);
|
||||
|
||||
expect(serverInfos).toHaveLength(2);
|
||||
|
||||
const persistentServer = serverInfos.find(s => s.name === 'persistent-server');
|
||||
const onDemandServer = serverInfos.find(s => s.name === 'on-demand-server');
|
||||
|
||||
expect(persistentServer?.connectionMode).toBe('persistent');
|
||||
expect(onDemandServer?.connectionMode).toBe('on-demand');
|
||||
expect(onDemandServer?.status).toBe('disconnected');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user