mirror of
https://github.com/arc53/DocsGPT.git
synced 2025-11-29 08:33:20 +00:00
feat: finalize remote mcp
This commit is contained in:
@@ -5,6 +5,7 @@ from typing import Any, Dict, List, Optional
|
|||||||
import requests
|
import requests
|
||||||
|
|
||||||
from application.agents.tools.base import Tool
|
from application.agents.tools.base import Tool
|
||||||
|
from application.security.encryption import decrypt_credentials
|
||||||
|
|
||||||
|
|
||||||
_mcp_session_cache = {}
|
_mcp_session_cache = {}
|
||||||
@@ -33,18 +34,12 @@ class MCPTool(Tool):
|
|||||||
self.auth_type = config.get("auth_type", "none")
|
self.auth_type = config.get("auth_type", "none")
|
||||||
self.timeout = config.get("timeout", 30)
|
self.timeout = config.get("timeout", 30)
|
||||||
|
|
||||||
# Decrypt credentials if they are encrypted
|
|
||||||
|
|
||||||
self.auth_credentials = {}
|
self.auth_credentials = {}
|
||||||
if config.get("encrypted_credentials") and user_id:
|
if config.get("encrypted_credentials") and user_id:
|
||||||
from application.security.encryption import decrypt_credentials
|
|
||||||
|
|
||||||
self.auth_credentials = decrypt_credentials(
|
self.auth_credentials = decrypt_credentials(
|
||||||
config["encrypted_credentials"], user_id
|
config["encrypted_credentials"], user_id
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# Fallback to unencrypted credentials (for backward compatibility)
|
|
||||||
|
|
||||||
self.auth_credentials = config.get("auth_credentials", {})
|
self.auth_credentials = config.get("auth_credentials", {})
|
||||||
self.available_tools = []
|
self.available_tools = []
|
||||||
self._session = requests.Session()
|
self._session = requests.Session()
|
||||||
@@ -52,10 +47,25 @@ class MCPTool(Tool):
|
|||||||
self._setup_authentication()
|
self._setup_authentication()
|
||||||
self._cache_key = self._generate_cache_key()
|
self._cache_key = self._generate_cache_key()
|
||||||
|
|
||||||
|
def _setup_authentication(self):
|
||||||
|
"""Setup authentication for the MCP server connection."""
|
||||||
|
if self.auth_type == "api_key":
|
||||||
|
api_key = self.auth_credentials.get("api_key", "")
|
||||||
|
header_name = self.auth_credentials.get("api_key_header", "X-API-Key")
|
||||||
|
if api_key:
|
||||||
|
self._session.headers.update({header_name: api_key})
|
||||||
|
elif self.auth_type == "bearer":
|
||||||
|
token = self.auth_credentials.get("bearer_token", "")
|
||||||
|
if token:
|
||||||
|
self._session.headers.update({"Authorization": f"Bearer {token}"})
|
||||||
|
elif self.auth_type == "basic":
|
||||||
|
username = self.auth_credentials.get("username", "")
|
||||||
|
password = self.auth_credentials.get("password", "")
|
||||||
|
if username and password:
|
||||||
|
self._session.auth = (username, password)
|
||||||
|
|
||||||
def _generate_cache_key(self) -> str:
|
def _generate_cache_key(self) -> str:
|
||||||
"""Generate a unique cache key for this MCP server configuration."""
|
"""Generate a unique cache key for this MCP server configuration."""
|
||||||
# Use server URL + auth info to create unique key
|
|
||||||
|
|
||||||
auth_key = ""
|
auth_key = ""
|
||||||
if self.auth_type == "bearer":
|
if self.auth_type == "bearer":
|
||||||
token = self.auth_credentials.get("bearer_token", "")
|
token = self.auth_credentials.get("bearer_token", "")
|
||||||
@@ -76,13 +86,9 @@ class MCPTool(Tool):
|
|||||||
|
|
||||||
if self._cache_key in _mcp_session_cache:
|
if self._cache_key in _mcp_session_cache:
|
||||||
session_data = _mcp_session_cache[self._cache_key]
|
session_data = _mcp_session_cache[self._cache_key]
|
||||||
# Check if session is less than 30 minutes old
|
if time.time() - session_data["created_at"] < 1800:
|
||||||
|
|
||||||
if time.time() - session_data["created_at"] < 1800: # 30 minutes
|
|
||||||
return session_data["session_id"]
|
return session_data["session_id"]
|
||||||
else:
|
else:
|
||||||
# Remove expired session
|
|
||||||
|
|
||||||
del _mcp_session_cache[self._cache_key]
|
del _mcp_session_cache[self._cache_key]
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -94,23 +100,6 @@ class MCPTool(Tool):
|
|||||||
"created_at": time.time(),
|
"created_at": time.time(),
|
||||||
}
|
}
|
||||||
|
|
||||||
def _setup_authentication(self):
|
|
||||||
"""Setup authentication for the MCP server connection."""
|
|
||||||
if self.auth_type == "api_key":
|
|
||||||
api_key = self.auth_credentials.get("api_key", "")
|
|
||||||
header_name = self.auth_credentials.get("api_key_header", "X-API-Key")
|
|
||||||
if api_key:
|
|
||||||
self._session.headers.update({header_name: api_key})
|
|
||||||
elif self.auth_type == "bearer":
|
|
||||||
token = self.auth_credentials.get("bearer_token", "")
|
|
||||||
if token:
|
|
||||||
self._session.headers.update({"Authorization": f"Bearer {token}"})
|
|
||||||
elif self.auth_type == "basic":
|
|
||||||
username = self.auth_credentials.get("username", "")
|
|
||||||
password = self.auth_credentials.get("password", "")
|
|
||||||
if username and password:
|
|
||||||
self._session.auth = (username, password)
|
|
||||||
|
|
||||||
def _initialize_mcp_connection(self) -> Dict:
|
def _initialize_mcp_connection(self) -> Dict:
|
||||||
"""
|
"""
|
||||||
Initialize MCP connection with the server, using cached session if available.
|
Initialize MCP connection with the server, using cached session if available.
|
||||||
@@ -264,10 +253,7 @@ class MCPTool(Tool):
|
|||||||
"""
|
"""
|
||||||
self._ensure_valid_session()
|
self._ensure_valid_session()
|
||||||
|
|
||||||
# Prepare call parameters for MCP protocol
|
|
||||||
|
|
||||||
call_params = {"name": action_name, "arguments": kwargs}
|
call_params = {"name": action_name, "arguments": kwargs}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = self._make_mcp_request("tools/call", call_params)
|
result = self._make_mcp_request("tools/call", call_params)
|
||||||
return result
|
return result
|
||||||
@@ -283,9 +269,6 @@ class MCPTool(Tool):
|
|||||||
"""
|
"""
|
||||||
actions = []
|
actions = []
|
||||||
for tool in self.available_tools:
|
for tool in self.available_tools:
|
||||||
# Parse MCP tool schema according to MCP specification
|
|
||||||
# Check multiple possible schema locations for compatibility
|
|
||||||
|
|
||||||
input_schema = (
|
input_schema = (
|
||||||
tool.get("inputSchema")
|
tool.get("inputSchema")
|
||||||
or tool.get("input_schema")
|
or tool.get("input_schema")
|
||||||
@@ -293,20 +276,14 @@ class MCPTool(Tool):
|
|||||||
or tool.get("parameters")
|
or tool.get("parameters")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Default empty schema if no inputSchema provided
|
|
||||||
|
|
||||||
parameters_schema = {
|
parameters_schema = {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {},
|
"properties": {},
|
||||||
"required": [],
|
"required": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
# Parse the inputSchema if it exists
|
|
||||||
|
|
||||||
if input_schema:
|
if input_schema:
|
||||||
if isinstance(input_schema, dict):
|
if isinstance(input_schema, dict):
|
||||||
# Handle standard JSON Schema format
|
|
||||||
|
|
||||||
if "properties" in input_schema:
|
if "properties" in input_schema:
|
||||||
parameters_schema = {
|
parameters_schema = {
|
||||||
"type": input_schema.get("type", "object"),
|
"type": input_schema.get("type", "object"),
|
||||||
@@ -314,14 +291,10 @@ class MCPTool(Tool):
|
|||||||
"required": input_schema.get("required", []),
|
"required": input_schema.get("required", []),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Add additional schema properties if they exist
|
|
||||||
|
|
||||||
for key in ["additionalProperties", "description"]:
|
for key in ["additionalProperties", "description"]:
|
||||||
if key in input_schema:
|
if key in input_schema:
|
||||||
parameters_schema[key] = input_schema[key]
|
parameters_schema[key] = input_schema[key]
|
||||||
else:
|
else:
|
||||||
# Might be properties directly at root level
|
|
||||||
|
|
||||||
parameters_schema["properties"] = input_schema
|
parameters_schema["properties"] = input_schema
|
||||||
action = {
|
action = {
|
||||||
"name": tool.get("name", ""),
|
"name": tool.get("name", ""),
|
||||||
@@ -331,64 +304,6 @@ class MCPTool(Tool):
|
|||||||
actions.append(action)
|
actions.append(action)
|
||||||
return actions
|
return actions
|
||||||
|
|
||||||
def get_config_requirements(self) -> Dict:
|
|
||||||
"""
|
|
||||||
Get configuration requirements for the MCP tool.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dictionary describing required configuration
|
|
||||||
"""
|
|
||||||
return {
|
|
||||||
"server_url": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "URL of the remote MCP server (e.g., https://api.example.com)",
|
|
||||||
"required": True,
|
|
||||||
},
|
|
||||||
"auth_type": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Authentication type",
|
|
||||||
"enum": ["none", "api_key", "bearer", "basic"],
|
|
||||||
"default": "none",
|
|
||||||
"required": True,
|
|
||||||
},
|
|
||||||
"auth_credentials": {
|
|
||||||
"type": "object",
|
|
||||||
"description": "Authentication credentials (varies by auth_type)",
|
|
||||||
"properties": {
|
|
||||||
"api_key": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "API key for api_key auth",
|
|
||||||
},
|
|
||||||
"header_name": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Header name for API key (default: X-API-Key)",
|
|
||||||
"default": "X-API-Key",
|
|
||||||
},
|
|
||||||
"token": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Bearer token for bearer auth",
|
|
||||||
},
|
|
||||||
"username": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Username for basic auth",
|
|
||||||
},
|
|
||||||
"password": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Password for basic auth",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"required": False,
|
|
||||||
},
|
|
||||||
"timeout": {
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Request timeout in seconds",
|
|
||||||
"default": 30,
|
|
||||||
"minimum": 1,
|
|
||||||
"maximum": 300,
|
|
||||||
"required": False,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_connection(self) -> Dict:
|
def test_connection(self) -> Dict:
|
||||||
"""
|
"""
|
||||||
Test the connection to the MCP server and validate functionality.
|
Test the connection to the MCP server and validate functionality.
|
||||||
@@ -411,9 +326,7 @@ class MCPTool(Tool):
|
|||||||
"message": message,
|
"message": message,
|
||||||
"tools_count": len(tools),
|
"tools_count": len(tools),
|
||||||
"session_id": self._mcp_session_id,
|
"session_id": self._mcp_session_id,
|
||||||
"tools": [
|
"tools": [tool.get("name", "unknown") for tool in tools[:5]],
|
||||||
tool.get("name", "unknown") for tool in tools[:5]
|
|
||||||
], # First 5 tool names
|
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {
|
return {
|
||||||
@@ -422,3 +335,32 @@ class MCPTool(Tool):
|
|||||||
"tools_count": 0,
|
"tools_count": 0,
|
||||||
"error_type": type(e).__name__,
|
"error_type": type(e).__name__,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def get_config_requirements(self) -> Dict:
|
||||||
|
return {
|
||||||
|
"server_url": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "URL of the remote MCP server (e.g., https://api.example.com)",
|
||||||
|
"required": True,
|
||||||
|
},
|
||||||
|
"auth_type": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Authentication type",
|
||||||
|
"enum": ["none", "api_key", "bearer", "basic"],
|
||||||
|
"default": "none",
|
||||||
|
"required": True,
|
||||||
|
},
|
||||||
|
"auth_credentials": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Authentication credentials (varies by auth_type)",
|
||||||
|
"required": False,
|
||||||
|
},
|
||||||
|
"timeout": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Request timeout in seconds",
|
||||||
|
"default": 30,
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 300,
|
||||||
|
"required": False,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ class ToolManager:
|
|||||||
module = importlib.import_module(f"application.agents.tools.{tool_name}")
|
module = importlib.import_module(f"application.agents.tools.{tool_name}")
|
||||||
for member_name, obj in inspect.getmembers(module, inspect.isclass):
|
for member_name, obj in inspect.getmembers(module, inspect.isclass):
|
||||||
if issubclass(obj, Tool) and obj is not Tool:
|
if issubclass(obj, Tool) and obj is not Tool:
|
||||||
# For MCP tools, pass the user_id for credential decryption
|
|
||||||
if tool_name == "mcp_tool" and user_id:
|
if tool_name == "mcp_tool" and user_id:
|
||||||
return obj(tool_config, user_id)
|
return obj(tool_config, user_id)
|
||||||
else:
|
else:
|
||||||
@@ -36,18 +35,11 @@ class ToolManager:
|
|||||||
|
|
||||||
def execute_action(self, tool_name, action_name, user_id=None, **kwargs):
|
def execute_action(self, tool_name, action_name, user_id=None, **kwargs):
|
||||||
if tool_name not in self.tools:
|
if tool_name not in self.tools:
|
||||||
# For MCP tools, they might not be pre-loaded, so load dynamically
|
|
||||||
if tool_name == "mcp_tool":
|
|
||||||
raise ValueError(f"Tool '{tool_name}' not loaded and no config provided for dynamic loading")
|
|
||||||
raise ValueError(f"Tool '{tool_name}' not loaded")
|
raise ValueError(f"Tool '{tool_name}' not loaded")
|
||||||
|
|
||||||
# For MCP tools, if user_id is provided, create a new instance with user context
|
|
||||||
if tool_name == "mcp_tool" and user_id:
|
if tool_name == "mcp_tool" and user_id:
|
||||||
# Load tool dynamically with user context for proper credential access
|
|
||||||
tool_config = self.config.get(tool_name, {})
|
tool_config = self.config.get(tool_name, {})
|
||||||
tool = self.load_tool(tool_name, tool_config, user_id)
|
tool = self.load_tool(tool_name, tool_config, user_id)
|
||||||
return tool.execute_action(action_name, **kwargs)
|
return tool.execute_action(action_name, **kwargs)
|
||||||
|
|
||||||
return self.tools[tool_name].execute_action(action_name, **kwargs)
|
return self.tools[tool_name].execute_action(action_name, **kwargs)
|
||||||
|
|
||||||
def get_all_actions_metadata(self):
|
def get_all_actions_metadata(self):
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ import json
|
|||||||
import math
|
import math
|
||||||
import os
|
import os
|
||||||
import secrets
|
import secrets
|
||||||
|
import tempfile
|
||||||
import uuid
|
import uuid
|
||||||
|
import zipfile
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from typing import Optional, Tuple
|
from typing import Optional, Tuple
|
||||||
import tempfile
|
|
||||||
import zipfile
|
|
||||||
from bson.binary import Binary, UuidRepresentation
|
from bson.binary import Binary, UuidRepresentation
|
||||||
from bson.dbref import DBRef
|
from bson.dbref import DBRef
|
||||||
from bson.objectid import ObjectId
|
from bson.objectid import ObjectId
|
||||||
@@ -24,7 +25,10 @@ from flask_restx import fields, inputs, Namespace, Resource
|
|||||||
from pymongo import ReturnDocument
|
from pymongo import ReturnDocument
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
|
|
||||||
|
from application.agents.tools.mcp_tool import MCPTool
|
||||||
|
|
||||||
from application.agents.tools.tool_manager import ToolManager
|
from application.agents.tools.tool_manager import ToolManager
|
||||||
|
from application.api import api
|
||||||
|
|
||||||
from application.api.user.tasks import (
|
from application.api.user.tasks import (
|
||||||
ingest,
|
ingest,
|
||||||
@@ -34,17 +38,17 @@ from application.api.user.tasks import (
|
|||||||
)
|
)
|
||||||
from application.core.mongo_db import MongoDB
|
from application.core.mongo_db import MongoDB
|
||||||
from application.core.settings import settings
|
from application.core.settings import settings
|
||||||
from application.api import api
|
from application.security.encryption import encrypt_credentials, decrypt_credentials
|
||||||
from application.storage.storage_creator import StorageCreator
|
from application.storage.storage_creator import StorageCreator
|
||||||
from application.tts.google_tts import GoogleTTS
|
from application.tts.google_tts import GoogleTTS
|
||||||
from application.utils import (
|
from application.utils import (
|
||||||
check_required_fields,
|
check_required_fields,
|
||||||
generate_image_url,
|
generate_image_url,
|
||||||
|
num_tokens_from_string,
|
||||||
safe_filename,
|
safe_filename,
|
||||||
validate_function_name,
|
validate_function_name,
|
||||||
validate_required_fields,
|
validate_required_fields,
|
||||||
)
|
)
|
||||||
from application.utils import num_tokens_from_string
|
|
||||||
from application.vectorstore.vector_creator import VectorCreator
|
from application.vectorstore.vector_creator import VectorCreator
|
||||||
|
|
||||||
storage = StorageCreator.get_storage()
|
storage = StorageCreator.get_storage()
|
||||||
@@ -3435,31 +3439,6 @@ class CreateTool(Resource):
|
|||||||
param_details["value"] = ""
|
param_details["value"] = ""
|
||||||
transformed_actions.append(action)
|
transformed_actions.append(action)
|
||||||
try:
|
try:
|
||||||
# Process config to encrypt credentials for MCP tools
|
|
||||||
config = data["config"]
|
|
||||||
if data["name"] == "mcp_tool":
|
|
||||||
from application.security.encryption import encrypt_credentials
|
|
||||||
|
|
||||||
# Extract credentials from config
|
|
||||||
credentials = {}
|
|
||||||
if config.get("auth_type") == "bearer":
|
|
||||||
credentials["bearer_token"] = config.get("bearer_token", "")
|
|
||||||
elif config.get("auth_type") == "api_key":
|
|
||||||
credentials["api_key"] = config.get("api_key", "")
|
|
||||||
credentials["api_key_header"] = config.get("api_key_header", "")
|
|
||||||
elif config.get("auth_type") == "basic":
|
|
||||||
credentials["username"] = config.get("username", "")
|
|
||||||
credentials["password"] = config.get("password", "")
|
|
||||||
|
|
||||||
# Encrypt credentials if any exist
|
|
||||||
if credentials:
|
|
||||||
config["encrypted_credentials"] = encrypt_credentials(
|
|
||||||
credentials, user
|
|
||||||
)
|
|
||||||
# Remove plaintext credentials from config
|
|
||||||
for key in credentials.keys():
|
|
||||||
config.pop(key, None)
|
|
||||||
|
|
||||||
new_tool = {
|
new_tool = {
|
||||||
"user": user,
|
"user": user,
|
||||||
"name": data["name"],
|
"name": data["name"],
|
||||||
@@ -3467,7 +3446,7 @@ class CreateTool(Resource):
|
|||||||
"description": data["description"],
|
"description": data["description"],
|
||||||
"customName": data.get("customName", ""),
|
"customName": data.get("customName", ""),
|
||||||
"actions": transformed_actions,
|
"actions": transformed_actions,
|
||||||
"config": config,
|
"config": data["config"],
|
||||||
"status": data["status"],
|
"status": data["status"],
|
||||||
}
|
}
|
||||||
resp = user_tools_collection.insert_one(new_tool)
|
resp = user_tools_collection.insert_one(new_tool)
|
||||||
@@ -3534,41 +3513,7 @@ class UpdateTool(Resource):
|
|||||||
),
|
),
|
||||||
400,
|
400,
|
||||||
)
|
)
|
||||||
|
update_data["config"] = data["config"]
|
||||||
# Handle MCP tool credential encryption
|
|
||||||
config = data["config"]
|
|
||||||
tool_name = data.get("name")
|
|
||||||
if not tool_name:
|
|
||||||
# Get the tool name from the database
|
|
||||||
existing_tool = user_tools_collection.find_one(
|
|
||||||
{"_id": ObjectId(data["id"]), "user": user}
|
|
||||||
)
|
|
||||||
tool_name = existing_tool.get("name") if existing_tool else None
|
|
||||||
|
|
||||||
if tool_name == "mcp_tool":
|
|
||||||
from application.security.encryption import encrypt_credentials
|
|
||||||
|
|
||||||
# Extract credentials from config
|
|
||||||
credentials = {}
|
|
||||||
if config.get("auth_type") == "bearer":
|
|
||||||
credentials["bearer_token"] = config.get("bearer_token", "")
|
|
||||||
elif config.get("auth_type") == "api_key":
|
|
||||||
credentials["api_key"] = config.get("api_key", "")
|
|
||||||
credentials["api_key_header"] = config.get("api_key_header", "")
|
|
||||||
elif config.get("auth_type") == "basic":
|
|
||||||
credentials["username"] = config.get("username", "")
|
|
||||||
credentials["password"] = config.get("password", "")
|
|
||||||
|
|
||||||
# Encrypt credentials if any exist
|
|
||||||
if credentials:
|
|
||||||
config["encrypted_credentials"] = encrypt_credentials(
|
|
||||||
credentials, user
|
|
||||||
)
|
|
||||||
# Remove plaintext credentials from config
|
|
||||||
for key in credentials.keys():
|
|
||||||
config.pop(key, None)
|
|
||||||
|
|
||||||
update_data["config"] = config
|
|
||||||
if "status" in data:
|
if "status" in data:
|
||||||
update_data["status"] = data["status"]
|
update_data["status"] = data["status"]
|
||||||
user_tools_collection.update_one(
|
user_tools_collection.update_one(
|
||||||
@@ -4142,74 +4087,55 @@ class DirectoryStructure(Resource):
|
|||||||
return make_response(jsonify({"success": False, "error": str(e)}), 500)
|
return make_response(jsonify({"success": False, "error": str(e)}), 500)
|
||||||
|
|
||||||
|
|
||||||
@user_ns.route("/api/mcp_servers")
|
@user_ns.route("/api/mcp_server/test")
|
||||||
class MCPServers(Resource):
|
class TestMCPServerConfig(Resource):
|
||||||
@api.doc(description="Get all MCP servers configured by the user")
|
@api.expect(
|
||||||
def get(self):
|
api.model(
|
||||||
|
"MCPServerTestModel",
|
||||||
|
{
|
||||||
|
"config": fields.Raw(
|
||||||
|
required=True, description="MCP server configuration to test"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@api.doc(description="Test MCP server connection with provided configuration")
|
||||||
|
def post(self):
|
||||||
decoded_token = request.decoded_token
|
decoded_token = request.decoded_token
|
||||||
if not decoded_token:
|
if not decoded_token:
|
||||||
return make_response(jsonify({"success": False}), 401)
|
return make_response(jsonify({"success": False}), 401)
|
||||||
|
|
||||||
user = decoded_token.get("sub")
|
user = decoded_token.get("sub")
|
||||||
|
data = request.get_json()
|
||||||
|
|
||||||
|
required_fields = ["config"]
|
||||||
|
missing_fields = check_required_fields(data, required_fields)
|
||||||
|
if missing_fields:
|
||||||
|
return missing_fields
|
||||||
try:
|
try:
|
||||||
# Find all MCP tools for this user
|
config = data["config"]
|
||||||
mcp_tools = user_tools_collection.find({"user": user, "name": "mcp_tool"})
|
|
||||||
|
|
||||||
servers = []
|
auth_credentials = {}
|
||||||
for tool in mcp_tools:
|
auth_type = config.get("auth_type", "none")
|
||||||
config = tool.get("config", {})
|
|
||||||
servers.append(
|
|
||||||
{
|
|
||||||
"id": str(tool["_id"]),
|
|
||||||
"name": tool.get("displayName", "MCP Server"),
|
|
||||||
"server_url": config.get("server_url", ""),
|
|
||||||
"auth_type": config.get("auth_type", "none"),
|
|
||||||
"status": tool.get("status", False),
|
|
||||||
"created_at": (
|
|
||||||
tool.get("_id").generation_time.isoformat()
|
|
||||||
if tool.get("_id")
|
|
||||||
else None
|
|
||||||
),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return make_response(jsonify({"success": True, "servers": servers}), 200)
|
if auth_type == "api_key" and "api_key" in config:
|
||||||
|
auth_credentials["api_key"] = config["api_key"]
|
||||||
|
if "api_key_header" in config:
|
||||||
|
auth_credentials["api_key_header"] = config["api_key_header"]
|
||||||
|
elif auth_type == "bearer" and "bearer_token" in config:
|
||||||
|
auth_credentials["bearer_token"] = config["bearer_token"]
|
||||||
|
elif auth_type == "basic":
|
||||||
|
if "username" in config:
|
||||||
|
auth_credentials["username"] = config["username"]
|
||||||
|
if "password" in config:
|
||||||
|
auth_credentials["password"] = config["password"]
|
||||||
|
|
||||||
except Exception as e:
|
test_config = config.copy()
|
||||||
current_app.logger.error(
|
test_config["auth_credentials"] = auth_credentials
|
||||||
f"Error retrieving MCP servers: {e}", exc_info=True
|
|
||||||
)
|
|
||||||
return make_response(jsonify({"success": False, "error": str(e)}), 500)
|
|
||||||
|
|
||||||
|
mcp_tool = MCPTool(test_config, user)
|
||||||
@user_ns.route("/api/mcp_server/<string:server_id>/test")
|
|
||||||
class TestMCPServer(Resource):
|
|
||||||
@api.doc(description="Test connection to an MCP server")
|
|
||||||
def post(self, server_id):
|
|
||||||
decoded_token = request.decoded_token
|
|
||||||
if not decoded_token:
|
|
||||||
return make_response(jsonify({"success": False}), 401)
|
|
||||||
|
|
||||||
user = decoded_token.get("sub")
|
|
||||||
try:
|
|
||||||
# Find the MCP tool
|
|
||||||
mcp_tool_doc = user_tools_collection.find_one(
|
|
||||||
{"_id": ObjectId(server_id), "user": user, "name": "mcp_tool"}
|
|
||||||
)
|
|
||||||
|
|
||||||
if not mcp_tool_doc:
|
|
||||||
return make_response(
|
|
||||||
jsonify({"success": False, "error": "MCP server not found"}), 404
|
|
||||||
)
|
|
||||||
|
|
||||||
# Load the tool and test connection
|
|
||||||
from application.agents.tools.mcp_tool import MCPTool
|
|
||||||
|
|
||||||
mcp_tool = MCPTool(mcp_tool_doc.get("config", {}), user)
|
|
||||||
result = mcp_tool.test_connection()
|
result = mcp_tool.test_connection()
|
||||||
|
|
||||||
return make_response(jsonify(result), 200)
|
return make_response(jsonify(result), 200)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
current_app.logger.error(f"Error testing MCP server: {e}", exc_info=True)
|
current_app.logger.error(f"Error testing MCP server: {e}", exc_info=True)
|
||||||
return make_response(
|
return make_response(
|
||||||
@@ -4220,38 +4146,86 @@ class TestMCPServer(Resource):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@user_ns.route("/api/mcp_server/<string:server_id>/tools")
|
@user_ns.route("/api/mcp_server/save")
|
||||||
class MCPServerTools(Resource):
|
class MCPServerSave(Resource):
|
||||||
@api.doc(description="Discover and get tools from an MCP server")
|
@api.expect(
|
||||||
def get(self, server_id):
|
api.model(
|
||||||
|
"MCPServerSaveModel",
|
||||||
|
{
|
||||||
|
"id": fields.String(
|
||||||
|
required=False, description="Tool ID for updates (optional)"
|
||||||
|
),
|
||||||
|
"displayName": fields.String(
|
||||||
|
required=True, description="Display name for the MCP server"
|
||||||
|
),
|
||||||
|
"config": fields.Raw(
|
||||||
|
required=True, description="MCP server configuration"
|
||||||
|
),
|
||||||
|
"status": fields.Boolean(
|
||||||
|
required=False, default=True, description="Tool status"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@api.doc(description="Create or update MCP server with automatic tool discovery")
|
||||||
|
def post(self):
|
||||||
decoded_token = request.decoded_token
|
decoded_token = request.decoded_token
|
||||||
if not decoded_token:
|
if not decoded_token:
|
||||||
return make_response(jsonify({"success": False}), 401)
|
return make_response(jsonify({"success": False}), 401)
|
||||||
|
|
||||||
user = decoded_token.get("sub")
|
user = decoded_token.get("sub")
|
||||||
try:
|
data = request.get_json()
|
||||||
# Find the MCP tool
|
|
||||||
mcp_tool_doc = user_tools_collection.find_one(
|
|
||||||
{"_id": ObjectId(server_id), "user": user, "name": "mcp_tool"}
|
|
||||||
)
|
|
||||||
|
|
||||||
if not mcp_tool_doc:
|
required_fields = ["displayName", "config"]
|
||||||
return make_response(
|
missing_fields = check_required_fields(data, required_fields)
|
||||||
jsonify({"success": False, "error": "MCP server not found"}), 404
|
if missing_fields:
|
||||||
|
return missing_fields
|
||||||
|
try:
|
||||||
|
config = data["config"]
|
||||||
|
|
||||||
|
auth_credentials = {}
|
||||||
|
auth_type = config.get("auth_type", "none")
|
||||||
|
if auth_type == "api_key":
|
||||||
|
if "api_key" in config and config["api_key"]:
|
||||||
|
auth_credentials["api_key"] = config["api_key"]
|
||||||
|
if "api_key_header" in config:
|
||||||
|
auth_credentials["api_key_header"] = config["api_key_header"]
|
||||||
|
elif auth_type == "bearer":
|
||||||
|
if "bearer_token" in config and config["bearer_token"]:
|
||||||
|
auth_credentials["bearer_token"] = config["bearer_token"]
|
||||||
|
elif auth_type == "basic":
|
||||||
|
if "username" in config and config["username"]:
|
||||||
|
auth_credentials["username"] = config["username"]
|
||||||
|
if "password" in config and config["password"]:
|
||||||
|
auth_credentials["password"] = config["password"]
|
||||||
|
mcp_config = config.copy()
|
||||||
|
mcp_config["auth_credentials"] = auth_credentials
|
||||||
|
|
||||||
|
if auth_type == "none" or auth_credentials:
|
||||||
|
mcp_tool = MCPTool(mcp_config, user)
|
||||||
|
mcp_tool.discover_tools()
|
||||||
|
actions_metadata = mcp_tool.get_actions_metadata()
|
||||||
|
else:
|
||||||
|
raise Exception(
|
||||||
|
"No valid credentials provided for the selected authentication type"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Load the tool and discover tools
|
storage_config = config.copy()
|
||||||
from application.agents.tools.mcp_tool import MCPTool
|
if auth_credentials:
|
||||||
|
encrypted_credentials_string = encrypt_credentials(
|
||||||
|
auth_credentials, user
|
||||||
|
)
|
||||||
|
storage_config["encrypted_credentials"] = encrypted_credentials_string
|
||||||
|
|
||||||
mcp_tool = MCPTool(mcp_tool_doc.get("config", {}), user)
|
for field in [
|
||||||
tools = mcp_tool.discover_tools()
|
"api_key",
|
||||||
|
"bearer_token",
|
||||||
# Get actions metadata and transform to match other tools format
|
"username",
|
||||||
actions_metadata = mcp_tool.get_actions_metadata()
|
"password",
|
||||||
|
"api_key_header",
|
||||||
|
]:
|
||||||
|
storage_config.pop(field, None)
|
||||||
transformed_actions = []
|
transformed_actions = []
|
||||||
|
|
||||||
for action in actions_metadata:
|
for action in actions_metadata:
|
||||||
# Add active flag and transform parameters
|
|
||||||
action["active"] = True
|
action["active"] = True
|
||||||
if "parameters" in action:
|
if "parameters" in action:
|
||||||
if "properties" in action["parameters"]:
|
if "properties" in action["parameters"]:
|
||||||
@@ -4261,77 +4235,53 @@ class MCPServerTools(Resource):
|
|||||||
param_details["filled_by_llm"] = True
|
param_details["filled_by_llm"] = True
|
||||||
param_details["value"] = ""
|
param_details["value"] = ""
|
||||||
transformed_actions.append(action)
|
transformed_actions.append(action)
|
||||||
|
tool_data = {
|
||||||
|
"name": "mcp_tool",
|
||||||
|
"displayName": data["displayName"],
|
||||||
|
"description": f"MCP Server: {storage_config.get('server_url', 'Unknown')}",
|
||||||
|
"config": storage_config,
|
||||||
|
"actions": transformed_actions,
|
||||||
|
"status": data.get("status", True),
|
||||||
|
"user": user,
|
||||||
|
}
|
||||||
|
|
||||||
# Update the stored actions in the database
|
tool_id = data.get("id")
|
||||||
user_tools_collection.update_one(
|
if tool_id:
|
||||||
{"_id": ObjectId(server_id)}, {"$set": {"actions": transformed_actions}}
|
result = user_tools_collection.update_one(
|
||||||
)
|
{"_id": ObjectId(tool_id), "user": user, "name": "mcp_tool"},
|
||||||
|
{"$set": {k: v for k, v in tool_data.items() if k != "user"}},
|
||||||
return make_response(
|
)
|
||||||
jsonify(
|
if result.matched_count == 0:
|
||||||
{"success": True, "tools": tools, "actions": transformed_actions}
|
return make_response(
|
||||||
),
|
jsonify(
|
||||||
200,
|
{
|
||||||
)
|
"success": False,
|
||||||
|
"error": "Tool not found or access denied",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
404,
|
||||||
|
)
|
||||||
|
response_data = {
|
||||||
|
"success": True,
|
||||||
|
"id": tool_id,
|
||||||
|
"message": f"MCP server updated successfully! Discovered {len(transformed_actions)} tools.",
|
||||||
|
"tools_count": len(transformed_actions),
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
result = user_tools_collection.insert_one(tool_data)
|
||||||
|
tool_id = str(result.inserted_id)
|
||||||
|
response_data = {
|
||||||
|
"success": True,
|
||||||
|
"id": tool_id,
|
||||||
|
"message": f"MCP server created successfully! Discovered {len(transformed_actions)} tools.",
|
||||||
|
"tools_count": len(transformed_actions),
|
||||||
|
}
|
||||||
|
return make_response(jsonify(response_data), 200)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
current_app.logger.error(f"Error discovering MCP tools: {e}", exc_info=True)
|
current_app.logger.error(f"Error saving MCP server: {e}", exc_info=True)
|
||||||
return make_response(
|
return make_response(
|
||||||
jsonify(
|
jsonify(
|
||||||
{"success": False, "error": f"Tool discovery failed: {str(e)}"}
|
{"success": False, "error": f"Failed to save MCP server: {str(e)}"}
|
||||||
),
|
|
||||||
500,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@user_ns.route("/api/mcp_server/<string:server_id>/tools/<string:action_name>")
|
|
||||||
class MCPServerToolAction(Resource):
|
|
||||||
@api.expect(
|
|
||||||
api.model(
|
|
||||||
"MCPToolActionModel",
|
|
||||||
{
|
|
||||||
"parameters": fields.Raw(
|
|
||||||
required=False, description="Parameters for the tool action"
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
)
|
|
||||||
@api.doc(description="Execute a specific tool action on an MCP server")
|
|
||||||
def post(self, server_id, action_name):
|
|
||||||
decoded_token = request.decoded_token
|
|
||||||
if not decoded_token:
|
|
||||||
return make_response(jsonify({"success": False}), 401)
|
|
||||||
|
|
||||||
user = decoded_token.get("sub")
|
|
||||||
data = request.get_json() or {}
|
|
||||||
parameters = data.get("parameters", {})
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Find the MCP tool
|
|
||||||
mcp_tool_doc = user_tools_collection.find_one(
|
|
||||||
{"_id": ObjectId(server_id), "user": user, "name": "mcp_tool"}
|
|
||||||
)
|
|
||||||
|
|
||||||
if not mcp_tool_doc:
|
|
||||||
return make_response(
|
|
||||||
jsonify({"success": False, "error": "MCP server not found"}), 404
|
|
||||||
)
|
|
||||||
|
|
||||||
# Load the tool and execute action
|
|
||||||
from application.agents.tools.mcp_tool import MCPTool
|
|
||||||
|
|
||||||
mcp_tool = MCPTool(mcp_tool_doc.get("config", {}), user)
|
|
||||||
result = mcp_tool.execute_action(action_name, **parameters)
|
|
||||||
|
|
||||||
return make_response(jsonify({"success": True, "result": result}), 200)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
current_app.logger.error(
|
|
||||||
f"Error executing MCP tool action: {e}", exc_info=True
|
|
||||||
)
|
|
||||||
return make_response(
|
|
||||||
jsonify(
|
|
||||||
{"success": False, "error": f"Action execution failed: {str(e)}"}
|
|
||||||
),
|
),
|
||||||
500,
|
500,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -109,6 +109,9 @@ class Settings(BaseSettings):
|
|||||||
|
|
||||||
JWT_SECRET_KEY: str = ""
|
JWT_SECRET_KEY: str = ""
|
||||||
|
|
||||||
|
# Encryption settings
|
||||||
|
ENCRYPTION_SECRET_KEY: str = "default-docsgpt-encryption-key"
|
||||||
|
|
||||||
|
|
||||||
path = Path(__file__).parent.parent.absolute()
|
path = Path(__file__).parent.parent.absolute()
|
||||||
settings = Settings(_env_file=path.joinpath(".env"), _env_file_encoding="utf-8")
|
settings = Settings(_env_file=path.joinpath(".env"), _env_file_encoding="utf-8")
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ anthropic==0.49.0
|
|||||||
boto3==1.38.18
|
boto3==1.38.18
|
||||||
beautifulsoup4==4.13.4
|
beautifulsoup4==4.13.4
|
||||||
celery==5.4.0
|
celery==5.4.0
|
||||||
|
cryptography==42.0.8
|
||||||
dataclasses-json==0.6.7
|
dataclasses-json==0.6.7
|
||||||
docx2txt==0.8
|
docx2txt==0.8
|
||||||
duckduckgo-search==7.5.2
|
duckduckgo-search==7.5.2
|
||||||
|
|||||||
@@ -1,97 +1,85 @@
|
|||||||
"""
|
|
||||||
Simple encryption utility for securely storing sensitive credentials.
|
|
||||||
Uses XOR encryption with a key derived from app secret and user ID.
|
|
||||||
Note: This is basic obfuscation. For production, consider using cryptography library.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
import hashlib
|
|
||||||
import os
|
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
from cryptography.hazmat.backends import default_backend
|
||||||
|
from cryptography.hazmat.primitives import hashes
|
||||||
|
from cryptography.hazmat.primitives.ciphers import algorithms, Cipher, modes
|
||||||
|
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
||||||
|
|
||||||
|
from application.core.settings import settings
|
||||||
|
|
||||||
|
|
||||||
def _get_encryption_key(user_id: str) -> bytes:
|
def _derive_key(user_id: str, salt: bytes) -> bytes:
|
||||||
"""
|
app_secret = settings.ENCRYPTION_SECRET_KEY
|
||||||
Generate a consistent encryption key for a specific user.
|
|
||||||
Uses app secret + user ID to create a unique key per user.
|
password = f"{app_secret}#{user_id}".encode()
|
||||||
"""
|
|
||||||
# Get app secret from environment or use a default (in production, always use env)
|
kdf = PBKDF2HMAC(
|
||||||
app_secret = os.environ.get(
|
algorithm=hashes.SHA256(),
|
||||||
"APP_SECRET_KEY", "default-docsgpt-secret-key-change-in-production"
|
length=32,
|
||||||
|
salt=salt,
|
||||||
|
iterations=100000,
|
||||||
|
backend=default_backend(),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Combine app secret with user ID for user-specific encryption
|
return kdf.derive(password)
|
||||||
combined = f"{app_secret}#{user_id}"
|
|
||||||
|
|
||||||
# Create a 32-byte key
|
|
||||||
key_material = hashlib.sha256(combined.encode()).digest()
|
|
||||||
|
|
||||||
return key_material
|
|
||||||
|
|
||||||
|
|
||||||
def _xor_encrypt_decrypt(data: bytes, key: bytes) -> bytes:
|
|
||||||
"""Simple XOR encryption/decryption."""
|
|
||||||
result = bytearray()
|
|
||||||
for i, byte in enumerate(data):
|
|
||||||
result.append(byte ^ key[i % len(key)])
|
|
||||||
return bytes(result)
|
|
||||||
|
|
||||||
|
|
||||||
def encrypt_credentials(credentials: dict, user_id: str) -> str:
|
def encrypt_credentials(credentials: dict, user_id: str) -> str:
|
||||||
"""
|
|
||||||
Encrypt credentials dictionary for secure storage.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
credentials: Dictionary containing sensitive data
|
|
||||||
user_id: User ID for creating user-specific encryption key
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Base64 encoded encrypted string
|
|
||||||
"""
|
|
||||||
if not credentials:
|
if not credentials:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
key = _get_encryption_key(user_id)
|
salt = os.urandom(16)
|
||||||
|
iv = os.urandom(16)
|
||||||
|
key = _derive_key(user_id, salt)
|
||||||
|
|
||||||
# Convert dict to JSON string and encrypt
|
|
||||||
json_str = json.dumps(credentials)
|
json_str = json.dumps(credentials)
|
||||||
encrypted_data = _xor_encrypt_decrypt(json_str.encode(), key)
|
|
||||||
|
|
||||||
# Return base64 encoded for storage
|
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
|
||||||
return base64.b64encode(encrypted_data).decode()
|
encryptor = cipher.encryptor()
|
||||||
|
|
||||||
|
padded_data = _pad_data(json_str.encode())
|
||||||
|
encrypted_data = encryptor.update(padded_data) + encryptor.finalize()
|
||||||
|
|
||||||
|
result = salt + iv + encrypted_data
|
||||||
|
return base64.b64encode(result).decode()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# If encryption fails, store empty string (will require re-auth)
|
|
||||||
print(f"Warning: Failed to encrypt credentials: {e}")
|
print(f"Warning: Failed to encrypt credentials: {e}")
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def decrypt_credentials(encrypted_data: str, user_id: str) -> dict:
|
def decrypt_credentials(encrypted_data: str, user_id: str) -> dict:
|
||||||
"""
|
|
||||||
Decrypt credentials from storage.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
encrypted_data: Base64 encoded encrypted string
|
|
||||||
user_id: User ID for creating user-specific encryption key
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dictionary containing decrypted credentials
|
|
||||||
"""
|
|
||||||
if not encrypted_data:
|
if not encrypted_data:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
key = _get_encryption_key(user_id)
|
data = base64.b64decode(encrypted_data.encode())
|
||||||
|
|
||||||
# Decode and decrypt
|
salt = data[:16]
|
||||||
encrypted_bytes = base64.b64decode(encrypted_data.encode())
|
iv = data[16:32]
|
||||||
decrypted_data = _xor_encrypt_decrypt(encrypted_bytes, key)
|
encrypted_content = data[32:]
|
||||||
|
|
||||||
|
key = _derive_key(user_id, salt)
|
||||||
|
|
||||||
|
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
|
||||||
|
decryptor = cipher.decryptor()
|
||||||
|
|
||||||
|
decrypted_padded = decryptor.update(encrypted_content) + decryptor.finalize()
|
||||||
|
decrypted_data = _unpad_data(decrypted_padded)
|
||||||
|
|
||||||
# Parse JSON back to dict
|
|
||||||
return json.loads(decrypted_data.decode())
|
return json.loads(decrypted_data.decode())
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# If decryption fails, return empty dict (will require re-auth)
|
|
||||||
print(f"Warning: Failed to decrypt credentials: {e}")
|
print(f"Warning: Failed to decrypt credentials: {e}")
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def _pad_data(data: bytes) -> bytes:
|
||||||
|
block_size = 16
|
||||||
|
padding_len = block_size - (len(data) % block_size)
|
||||||
|
padding = bytes([padding_len]) * padding_len
|
||||||
|
return data + padding
|
||||||
|
|
||||||
|
|
||||||
|
def _unpad_data(data: bytes) -> bytes:
|
||||||
|
padding_len = data[-1]
|
||||||
|
return data[:-padding_len]
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="64" height="64" color="#000000" fill="none">
|
||||||
|
<path d="M3.49994 11.7501L11.6717 3.57855C12.7762 2.47398 14.5672 2.47398 15.6717 3.57855C16.7762 4.68312 16.7762 6.47398 15.6717 7.57855M15.6717 7.57855L9.49994 13.7501M15.6717 7.57855C16.7762 6.47398 18.5672 6.47398 19.6717 7.57855C20.7762 8.68312 20.7762 10.474 19.6717 11.5785L12.7072 18.543C12.3167 18.9335 12.3167 19.5667 12.7072 19.9572L13.9999 21.2499" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||||
|
<path d="M17.4999 9.74921L11.3282 15.921C10.2237 17.0255 8.43272 17.0255 7.32823 15.921C6.22373 14.8164 6.22373 13.0255 7.32823 11.921L13.4999 5.74939" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||||
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 0 B After Width: | Height: | Size: 831 B |
@@ -56,6 +56,8 @@ const endpoints = {
|
|||||||
DIRECTORY_STRUCTURE: (docId: string) =>
|
DIRECTORY_STRUCTURE: (docId: string) =>
|
||||||
`/api/directory_structure?id=${docId}`,
|
`/api/directory_structure?id=${docId}`,
|
||||||
MANAGE_SOURCE_FILES: '/api/manage_source_files',
|
MANAGE_SOURCE_FILES: '/api/manage_source_files',
|
||||||
|
MCP_TEST_CONNECTION: '/api/mcp_server/test',
|
||||||
|
MCP_SAVE_SERVER: '/api/mcp_server/save',
|
||||||
},
|
},
|
||||||
CONVERSATION: {
|
CONVERSATION: {
|
||||||
ANSWER: '/api/answer',
|
ANSWER: '/api/answer',
|
||||||
|
|||||||
@@ -89,7 +89,10 @@ const userService = {
|
|||||||
path?: string,
|
path?: string,
|
||||||
search?: string,
|
search?: string,
|
||||||
): Promise<any> =>
|
): Promise<any> =>
|
||||||
apiClient.get(endpoints.USER.GET_CHUNKS(docId, page, perPage, path, search), token),
|
apiClient.get(
|
||||||
|
endpoints.USER.GET_CHUNKS(docId, page, perPage, path, search),
|
||||||
|
token,
|
||||||
|
),
|
||||||
addChunk: (data: any, token: string | null): Promise<any> =>
|
addChunk: (data: any, token: string | null): Promise<any> =>
|
||||||
apiClient.post(endpoints.USER.ADD_CHUNK, data, token),
|
apiClient.post(endpoints.USER.ADD_CHUNK, data, token),
|
||||||
deleteChunk: (
|
deleteChunk: (
|
||||||
@@ -104,6 +107,10 @@ const userService = {
|
|||||||
apiClient.get(endpoints.USER.DIRECTORY_STRUCTURE(docId), token),
|
apiClient.get(endpoints.USER.DIRECTORY_STRUCTURE(docId), token),
|
||||||
manageSourceFiles: (data: FormData, token: string | null): Promise<any> =>
|
manageSourceFiles: (data: FormData, token: string | null): Promise<any> =>
|
||||||
apiClient.postFormData(endpoints.USER.MANAGE_SOURCE_FILES, data, token),
|
apiClient.postFormData(endpoints.USER.MANAGE_SOURCE_FILES, data, token),
|
||||||
|
testMCPConnection: (data: any, token: string | null): Promise<any> =>
|
||||||
|
apiClient.post(endpoints.USER.MCP_TEST_CONNECTION, data, token),
|
||||||
|
saveMCPServer: (data: any, token: string | null): Promise<any> =>
|
||||||
|
apiClient.post(endpoints.USER.MCP_SAVE_SERVER, data, token),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default userService;
|
export default userService;
|
||||||
|
|||||||
@@ -187,47 +187,24 @@
|
|||||||
"regularTools": "Regular Tools",
|
"regularTools": "Regular Tools",
|
||||||
"mcpTools": "MCP Tools",
|
"mcpTools": "MCP Tools",
|
||||||
"mcp": {
|
"mcp": {
|
||||||
"title": "MCP (Model Context Protocol) Servers",
|
|
||||||
"description": "Connect to remote MCP servers to access their tools and capabilities. Only remote servers are supported.",
|
|
||||||
"addServer": "Add MCP Server",
|
"addServer": "Add MCP Server",
|
||||||
"editServer": "Edit Server",
|
"editServer": "Edit Server",
|
||||||
"deleteServer": "Delete Server",
|
|
||||||
"delete": "Delete",
|
|
||||||
"serverName": "Server Name",
|
"serverName": "Server Name",
|
||||||
"serverUrl": "Server URL",
|
"serverUrl": "Server URL",
|
||||||
"authType": "Authentication Type",
|
|
||||||
"apiKey": "API Key",
|
|
||||||
"headerName": "Header Name",
|
"headerName": "Header Name",
|
||||||
"bearerToken": "Bearer Token",
|
|
||||||
"username": "Username",
|
|
||||||
"password": "Password",
|
|
||||||
"timeout": "Timeout (seconds)",
|
"timeout": "Timeout (seconds)",
|
||||||
"testConnection": "Test Connection",
|
"testConnection": "Test Connection",
|
||||||
"testing": "Testing...",
|
"testing": "Testing...",
|
||||||
"saving": "Saving...",
|
"saving": "Saving...",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"backToServers": "← Back to Servers",
|
|
||||||
"availableTools": "Available Tools",
|
|
||||||
"refreshTools": "Refresh Tools",
|
|
||||||
"refreshing": "Refreshing...",
|
|
||||||
"serverDisabled": "Server is disabled. Enable it to view available tools.",
|
|
||||||
"noToolsFound": "No tools found on this server.",
|
|
||||||
"noServersFound": "No MCP servers configured.",
|
|
||||||
"addFirstServer": "Add your first MCP server to get started.",
|
|
||||||
"parameters": "Parameters",
|
|
||||||
"active": "Active",
|
|
||||||
"inactive": "Inactive",
|
|
||||||
"noAuth": "No Authentication",
|
"noAuth": "No Authentication",
|
||||||
"toggleServer": "Toggle {{serverName}}",
|
|
||||||
"deleteWarning": "Are you sure you want to delete the MCP server \"{{serverName}}\"? This action cannot be undone.",
|
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"serverName": "My MCP Server",
|
|
||||||
"serverUrl": "https://api.example.com",
|
"serverUrl": "https://api.example.com",
|
||||||
"apiKey": "Enter your API key",
|
"apiKey": "Your secret API key",
|
||||||
"bearerToken": "Enter your bearer token",
|
"bearerToken": "Your secret token",
|
||||||
"username": "Enter username",
|
"username": "Your username",
|
||||||
"password": "Enter password"
|
"password": "Your password"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"nameRequired": "Server name is required",
|
"nameRequired": "Server name is required",
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { useRef, useState } from 'react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
import apiClient from '../api/client';
|
|
||||||
import userService from '../api/services/userService';
|
import userService from '../api/services/userService';
|
||||||
|
import Dropdown from '../components/Dropdown';
|
||||||
import Input from '../components/Input';
|
import Input from '../components/Input';
|
||||||
import Spinner from '../components/Spinner';
|
import Spinner from '../components/Spinner';
|
||||||
import { useOutsideAlerter } from '../hooks';
|
import { useOutsideAlerter } from '../hooks';
|
||||||
@@ -19,10 +19,10 @@ interface MCPServerModalProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const authTypes = [
|
const authTypes = [
|
||||||
{ value: 'none', label: 'No Authentication' },
|
{ label: 'No Authentication', value: 'none' },
|
||||||
{ value: 'api_key', label: 'API Key' },
|
{ label: 'API Key', value: 'api_key' },
|
||||||
{ value: 'bearer', label: 'Bearer Token' },
|
{ label: 'Bearer Token', value: 'bearer' },
|
||||||
{ value: 'basic', label: 'Basic Authentication' },
|
// { label: 'Basic Authentication', value: 'basic' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function MCPServerModal({
|
export default function MCPServerModal({
|
||||||
@@ -36,7 +36,7 @@ export default function MCPServerModal({
|
|||||||
const modalRef = useRef<HTMLDivElement>(null);
|
const modalRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
name: server?.name || 'My MCP Server',
|
name: server?.displayName || 'My MCP Server',
|
||||||
server_url: server?.server_url || '',
|
server_url: server?.server_url || '',
|
||||||
auth_type: server?.auth_type || 'none',
|
auth_type: server?.auth_type || 'none',
|
||||||
api_key: '',
|
api_key: '',
|
||||||
@@ -44,7 +44,7 @@ export default function MCPServerModal({
|
|||||||
bearer_token: '',
|
bearer_token: '',
|
||||||
username: '',
|
username: '',
|
||||||
password: '',
|
password: '',
|
||||||
timeout: 30,
|
timeout: server?.timeout || 30,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@@ -79,15 +79,37 @@ export default function MCPServerModal({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const validateForm = () => {
|
const validateForm = () => {
|
||||||
|
const requiredFields: { [key: string]: boolean } = {
|
||||||
|
name: !formData.name.trim(),
|
||||||
|
server_url: !formData.server_url.trim(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const authFieldChecks: { [key: string]: () => void } = {
|
||||||
|
api_key: () => {
|
||||||
|
if (!formData.api_key.trim())
|
||||||
|
newErrors.api_key = t('settings.tools.mcp.errors.apiKeyRequired');
|
||||||
|
},
|
||||||
|
bearer: () => {
|
||||||
|
if (!formData.bearer_token.trim())
|
||||||
|
newErrors.bearer_token = t('settings.tools.mcp.errors.tokenRequired');
|
||||||
|
},
|
||||||
|
basic: () => {
|
||||||
|
if (!formData.username.trim())
|
||||||
|
newErrors.username = t('settings.tools.mcp.errors.usernameRequired');
|
||||||
|
if (!formData.password.trim())
|
||||||
|
newErrors.password = t('settings.tools.mcp.errors.passwordRequired');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const newErrors: { [key: string]: string } = {};
|
const newErrors: { [key: string]: string } = {};
|
||||||
|
Object.entries(requiredFields).forEach(([field, isEmpty]) => {
|
||||||
|
if (isEmpty)
|
||||||
|
newErrors[field] = t(
|
||||||
|
`settings.tools.mcp.errors.${field === 'name' ? 'nameRequired' : 'urlRequired'}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
if (!formData.name.trim()) {
|
if (formData.server_url.trim()) {
|
||||||
newErrors.name = t('settings.tools.mcp.errors.nameRequired');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!formData.server_url.trim()) {
|
|
||||||
newErrors.server_url = t('settings.tools.mcp.errors.urlRequired');
|
|
||||||
} else {
|
|
||||||
try {
|
try {
|
||||||
new URL(formData.server_url);
|
new URL(formData.server_url);
|
||||||
} catch {
|
} catch {
|
||||||
@@ -95,22 +117,15 @@ export default function MCPServerModal({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (formData.auth_type === 'api_key' && !formData.api_key.trim()) {
|
const timeoutValue = formData.timeout === '' ? 30 : formData.timeout;
|
||||||
newErrors.api_key = t('settings.tools.mcp.errors.apiKeyRequired');
|
if (
|
||||||
}
|
typeof timeoutValue === 'number' &&
|
||||||
|
(timeoutValue < 1 || timeoutValue > 300)
|
||||||
|
)
|
||||||
|
newErrors.timeout = 'Timeout must be between 1 and 300 seconds';
|
||||||
|
|
||||||
if (formData.auth_type === 'bearer' && !formData.bearer_token.trim()) {
|
if (authFieldChecks[formData.auth_type])
|
||||||
newErrors.bearer_token = t('settings.tools.mcp.errors.tokenRequired');
|
authFieldChecks[formData.auth_type]();
|
||||||
}
|
|
||||||
|
|
||||||
if (formData.auth_type === 'basic') {
|
|
||||||
if (!formData.username.trim()) {
|
|
||||||
newErrors.username = t('settings.tools.mcp.errors.usernameRequired');
|
|
||||||
}
|
|
||||||
if (!formData.password.trim()) {
|
|
||||||
newErrors.password = t('settings.tools.mcp.errors.passwordRequired');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setErrors(newErrors);
|
setErrors(newErrors);
|
||||||
return Object.keys(newErrors).length === 0;
|
return Object.keys(newErrors).length === 0;
|
||||||
@@ -128,10 +143,9 @@ export default function MCPServerModal({
|
|||||||
const config: any = {
|
const config: any = {
|
||||||
server_url: formData.server_url.trim(),
|
server_url: formData.server_url.trim(),
|
||||||
auth_type: formData.auth_type,
|
auth_type: formData.auth_type,
|
||||||
timeout: formData.timeout,
|
timeout: formData.timeout === '' ? 30 : formData.timeout,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add credentials directly to config for encryption
|
|
||||||
if (formData.auth_type === 'api_key') {
|
if (formData.auth_type === 'api_key') {
|
||||||
config.api_key = formData.api_key.trim();
|
config.api_key = formData.api_key.trim();
|
||||||
config.api_key_header = formData.header_name.trim() || 'X-API-Key';
|
config.api_key_header = formData.header_name.trim() || 'X-API-Key';
|
||||||
@@ -141,59 +155,19 @@ export default function MCPServerModal({
|
|||||||
config.username = formData.username.trim();
|
config.username = formData.username.trim();
|
||||||
config.password = formData.password.trim();
|
config.password = formData.password.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
};
|
};
|
||||||
|
|
||||||
const testConnection = async () => {
|
const testConnection = async () => {
|
||||||
if (!validateForm()) return;
|
if (!validateForm()) return;
|
||||||
|
|
||||||
setTesting(true);
|
setTesting(true);
|
||||||
setTestResult(null);
|
setTestResult(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Create a temporary tool to test
|
|
||||||
const config = buildToolConfig();
|
const config = buildToolConfig();
|
||||||
|
const response = await userService.testMCPConnection({ config }, token);
|
||||||
const testData = {
|
|
||||||
name: 'mcp_tool',
|
|
||||||
displayName: formData.name,
|
|
||||||
description: 'MCP Server Connection',
|
|
||||||
config,
|
|
||||||
actions: [],
|
|
||||||
status: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = await userService.createTool(testData, token);
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (response.ok && result.id) {
|
setTestResult(result);
|
||||||
// Test the connection
|
|
||||||
try {
|
|
||||||
const testResponse = await apiClient.post(
|
|
||||||
`/api/mcp_server/${result.id}/test`,
|
|
||||||
{},
|
|
||||||
token,
|
|
||||||
);
|
|
||||||
const testData = await testResponse.json();
|
|
||||||
setTestResult(testData);
|
|
||||||
|
|
||||||
// Clean up the temporary tool
|
|
||||||
await userService.deleteTool({ id: result.id }, token);
|
|
||||||
} catch (error) {
|
|
||||||
setTestResult({
|
|
||||||
success: false,
|
|
||||||
message: t('settings.tools.mcp.errors.testFailed'),
|
|
||||||
});
|
|
||||||
// Clean up the temporary tool
|
|
||||||
await userService.deleteTool({ id: result.id }, token);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setTestResult({
|
|
||||||
success: false,
|
|
||||||
message: t('settings.tools.mcp.errors.testFailed'),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setTestResult({
|
setTestResult({
|
||||||
success: false,
|
success: false,
|
||||||
@@ -206,73 +180,32 @@ export default function MCPServerModal({
|
|||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
if (!validateForm()) return;
|
if (!validateForm()) return;
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const config = buildToolConfig();
|
const config = buildToolConfig();
|
||||||
|
const serverData = {
|
||||||
const toolData = {
|
|
||||||
name: 'mcp_tool',
|
|
||||||
displayName: formData.name,
|
displayName: formData.name,
|
||||||
description: `MCP Server: ${formData.server_url}`,
|
|
||||||
config,
|
config,
|
||||||
actions: [], // Will be populated after tool creation
|
|
||||||
status: true,
|
status: true,
|
||||||
|
...(server?.id && { id: server.id }),
|
||||||
};
|
};
|
||||||
|
|
||||||
let toolId: string;
|
const response = await userService.saveMCPServer(serverData, token);
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
if (server) {
|
if (response.ok && result.success) {
|
||||||
// Update existing server
|
setTestResult({
|
||||||
await userService.updateTool({ id: server.id, ...toolData }, token);
|
success: true,
|
||||||
toolId = server.id;
|
message: result.message,
|
||||||
|
});
|
||||||
|
onServerSaved();
|
||||||
|
setModalState('INACTIVE');
|
||||||
|
resetForm();
|
||||||
} else {
|
} else {
|
||||||
// Create new server
|
setErrors({
|
||||||
const response = await userService.createTool(toolData, token);
|
general: result.error || t('settings.tools.mcp.errors.saveFailed'),
|
||||||
const result = await response.json();
|
});
|
||||||
toolId = result.id;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now fetch the MCP tools and update the actions
|
|
||||||
try {
|
|
||||||
const toolsResponse = await apiClient.get(
|
|
||||||
`/api/mcp_server/${toolId}/tools`,
|
|
||||||
token,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (toolsResponse.success && toolsResponse.actions) {
|
|
||||||
// Update the tool with discovered actions (already formatted by backend)
|
|
||||||
await userService.updateTool(
|
|
||||||
{
|
|
||||||
id: toolId,
|
|
||||||
...toolData,
|
|
||||||
actions: toolsResponse.actions,
|
|
||||||
},
|
|
||||||
token,
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`Successfully discovered and saved ${toolsResponse.actions.length} MCP tools`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Show success message with tool count
|
|
||||||
setTestResult({
|
|
||||||
success: true,
|
|
||||||
message: `MCP server saved successfully! Discovered ${toolsResponse.actions.length} tools.`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(
|
|
||||||
'Warning: Could not fetch MCP tools immediately after creation:',
|
|
||||||
error,
|
|
||||||
);
|
|
||||||
// Don't fail the save operation if tool discovery fails
|
|
||||||
}
|
|
||||||
|
|
||||||
onServerSaved();
|
|
||||||
setModalState('INACTIVE');
|
|
||||||
resetForm();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving MCP server:', error);
|
console.error('Error saving MCP server:', error);
|
||||||
setErrors({ general: t('settings.tools.mcp.errors.saveFailed') });
|
setErrors({ general: t('settings.tools.mcp.errors.saveFailed') });
|
||||||
@@ -285,52 +218,52 @@ export default function MCPServerModal({
|
|||||||
switch (formData.auth_type) {
|
switch (formData.auth_type) {
|
||||||
case 'api_key':
|
case 'api_key':
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="mb-10">
|
||||||
<div>
|
<div className="mt-6">
|
||||||
<label className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
||||||
{t('settings.tools.mcp.apiKey')}
|
|
||||||
</label>
|
|
||||||
<Input
|
<Input
|
||||||
name="api_key"
|
name="api_key"
|
||||||
type="text"
|
type="text"
|
||||||
|
className="rounded-md"
|
||||||
value={formData.api_key}
|
value={formData.api_key}
|
||||||
onChange={(e) => handleInputChange('api_key', e.target.value)}
|
onChange={(e) => handleInputChange('api_key', e.target.value)}
|
||||||
placeholder={t('settings.tools.mcp.placeholders.apiKey')}
|
placeholder={t('settings.tools.mcp.placeholders.apiKey')}
|
||||||
|
borderVariant="thin"
|
||||||
|
labelBgClassName="bg-white dark:bg-charleston-green-2"
|
||||||
/>
|
/>
|
||||||
{errors.api_key && (
|
{errors.api_key && (
|
||||||
<p className="mt-1 text-sm text-red-600">{errors.api_key}</p>
|
<p className="mt-1 text-sm text-red-600">{errors.api_key}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="mt-5">
|
||||||
<label className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
||||||
{t('settings.tools.mcp.headerName')}
|
|
||||||
</label>
|
|
||||||
<Input
|
<Input
|
||||||
name="header_name"
|
name="header_name"
|
||||||
type="text"
|
type="text"
|
||||||
|
className="rounded-md"
|
||||||
value={formData.header_name}
|
value={formData.header_name}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
handleInputChange('header_name', e.target.value)
|
handleInputChange('header_name', e.target.value)
|
||||||
}
|
}
|
||||||
placeholder="X-API-Key"
|
placeholder={t('settings.tools.mcp.headerName')}
|
||||||
|
borderVariant="thin"
|
||||||
|
labelBgClassName="bg-white dark:bg-charleston-green-2"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
case 'bearer':
|
case 'bearer':
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="mb-10">
|
||||||
<label className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
||||||
{t('settings.tools.mcp.bearerToken')}
|
|
||||||
</label>
|
|
||||||
<Input
|
<Input
|
||||||
name="bearer_token"
|
name="bearer_token"
|
||||||
type="text"
|
type="text"
|
||||||
|
className="rounded-md"
|
||||||
value={formData.bearer_token}
|
value={formData.bearer_token}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
handleInputChange('bearer_token', e.target.value)
|
handleInputChange('bearer_token', e.target.value)
|
||||||
}
|
}
|
||||||
placeholder={t('settings.tools.mcp.placeholders.bearerToken')}
|
placeholder={t('settings.tools.mcp.placeholders.bearerToken')}
|
||||||
|
borderVariant="thin"
|
||||||
|
labelBgClassName="bg-white dark:bg-charleston-green-2"
|
||||||
/>
|
/>
|
||||||
{errors.bearer_token && (
|
{errors.bearer_token && (
|
||||||
<p className="mt-1 text-sm text-red-600">{errors.bearer_token}</p>
|
<p className="mt-1 text-sm text-red-600">{errors.bearer_token}</p>
|
||||||
@@ -339,32 +272,32 @@ export default function MCPServerModal({
|
|||||||
);
|
);
|
||||||
case 'basic':
|
case 'basic':
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="mb-10">
|
||||||
<div>
|
<div className="mt-6">
|
||||||
<label className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
||||||
{t('settings.tools.mcp.username')}
|
|
||||||
</label>
|
|
||||||
<Input
|
<Input
|
||||||
name="username"
|
name="username"
|
||||||
type="text"
|
type="text"
|
||||||
|
className="rounded-md"
|
||||||
value={formData.username}
|
value={formData.username}
|
||||||
onChange={(e) => handleInputChange('username', e.target.value)}
|
onChange={(e) => handleInputChange('username', e.target.value)}
|
||||||
placeholder={t('settings.tools.mcp.placeholders.username')}
|
placeholder={t('settings.tools.mcp.username')}
|
||||||
|
borderVariant="thin"
|
||||||
|
labelBgClassName="bg-white dark:bg-charleston-green-2"
|
||||||
/>
|
/>
|
||||||
{errors.username && (
|
{errors.username && (
|
||||||
<p className="mt-1 text-sm text-red-600">{errors.username}</p>
|
<p className="mt-1 text-sm text-red-600">{errors.username}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="mt-5">
|
||||||
<label className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
||||||
{t('settings.tools.mcp.password')}
|
|
||||||
</label>
|
|
||||||
<Input
|
<Input
|
||||||
name="password"
|
name="password"
|
||||||
type="text"
|
type="text"
|
||||||
|
className="rounded-md"
|
||||||
value={formData.password}
|
value={formData.password}
|
||||||
onChange={(e) => handleInputChange('password', e.target.value)}
|
onChange={(e) => handleInputChange('password', e.target.value)}
|
||||||
placeholder={t('settings.tools.mcp.placeholders.password')}
|
placeholder={t('settings.tools.mcp.password')}
|
||||||
|
borderVariant="thin"
|
||||||
|
labelBgClassName="bg-white dark:bg-charleston-green-2"
|
||||||
/>
|
/>
|
||||||
{errors.password && (
|
{errors.password && (
|
||||||
<p className="mt-1 text-sm text-red-600">{errors.password}</p>
|
<p className="mt-1 text-sm text-red-600">{errors.password}</p>
|
||||||
@@ -394,17 +327,17 @@ export default function MCPServerModal({
|
|||||||
: t('settings.tools.mcp.addServer')}
|
: t('settings.tools.mcp.addServer')}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex-1 px-6">
|
||||||
<div className="flex-1 overflow-auto px-6">
|
<div className="space-y-6 py-6">
|
||||||
<div className="space-y-6">
|
|
||||||
<div>
|
<div>
|
||||||
<Input
|
<Input
|
||||||
name="name"
|
name="name"
|
||||||
type="text"
|
type="text"
|
||||||
|
className="rounded-md"
|
||||||
value={formData.name}
|
value={formData.name}
|
||||||
onChange={(e) => handleInputChange('name', e.target.value)}
|
onChange={(e) => handleInputChange('name', e.target.value)}
|
||||||
borderVariant="thin"
|
borderVariant="thin"
|
||||||
placeholder={t('settings.tools.mcp.placeholders.serverName')}
|
placeholder={t('settings.tools.mcp.serverName')}
|
||||||
labelBgClassName="bg-white dark:bg-charleston-green-2"
|
labelBgClassName="bg-white dark:bg-charleston-green-2"
|
||||||
/>
|
/>
|
||||||
{errors.name && (
|
{errors.name && (
|
||||||
@@ -413,17 +346,17 @@ export default function MCPServerModal({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
||||||
{t('settings.tools.mcp.serverUrl')}
|
|
||||||
</label>
|
|
||||||
<Input
|
<Input
|
||||||
name="server_url"
|
name="server_url"
|
||||||
type="text"
|
type="text"
|
||||||
|
className="rounded-md"
|
||||||
value={formData.server_url}
|
value={formData.server_url}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
handleInputChange('server_url', e.target.value)
|
handleInputChange('server_url', e.target.value)
|
||||||
}
|
}
|
||||||
placeholder={t('settings.tools.mcp.placeholders.serverUrl')}
|
placeholder={t('settings.tools.mcp.serverUrl')}
|
||||||
|
borderVariant="thin"
|
||||||
|
labelBgClassName="bg-white dark:bg-charleston-green-2"
|
||||||
/>
|
/>
|
||||||
{errors.server_url && (
|
{errors.server_url && (
|
||||||
<p className="mt-1 text-sm text-red-600">
|
<p className="mt-1 text-sm text-red-600">
|
||||||
@@ -432,106 +365,114 @@ export default function MCPServerModal({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<Dropdown
|
||||||
<label className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
placeholder={t('settings.tools.mcp.authType')}
|
||||||
{t('settings.tools.mcp.authType')}
|
selectedValue={
|
||||||
</label>
|
authTypes.find((type) => type.value === formData.auth_type)
|
||||||
<select
|
?.label || null
|
||||||
value={formData.auth_type}
|
}
|
||||||
onChange={(e) =>
|
onSelect={(selection: { label: string; value: string }) => {
|
||||||
handleInputChange('auth_type', e.target.value)
|
handleInputChange('auth_type', selection.value);
|
||||||
}
|
}}
|
||||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
options={authTypes}
|
||||||
>
|
size="w-full"
|
||||||
{authTypes.map((type) => (
|
rounded="3xl"
|
||||||
<option key={type.value} value={type.value}>
|
border="border"
|
||||||
{type.label}
|
/>
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{renderAuthFields()}
|
{renderAuthFields()}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
||||||
{t('settings.tools.mcp.timeout')}
|
|
||||||
</label>
|
|
||||||
<Input
|
<Input
|
||||||
name="timeout"
|
name="timeout"
|
||||||
type="number"
|
type="number"
|
||||||
|
className="rounded-md"
|
||||||
value={formData.timeout}
|
value={formData.timeout}
|
||||||
onChange={(e) =>
|
onChange={(e) => {
|
||||||
handleInputChange('timeout', parseInt(e.target.value) || 30)
|
const value = e.target.value;
|
||||||
}
|
if (value === '') {
|
||||||
placeholder="30"
|
handleInputChange('timeout', '');
|
||||||
|
} else {
|
||||||
|
const numValue = parseInt(value);
|
||||||
|
if (!isNaN(numValue) && numValue >= 1) {
|
||||||
|
handleInputChange('timeout', numValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder={t('settings.tools.mcp.timeout')}
|
||||||
|
borderVariant="thin"
|
||||||
|
labelBgClassName="bg-white dark:bg-charleston-green-2"
|
||||||
/>
|
/>
|
||||||
|
{errors.timeout && (
|
||||||
|
<p className="mt-2 text-sm text-red-600">{errors.timeout}</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{testResult && (
|
{testResult && (
|
||||||
<div
|
<div
|
||||||
className={`rounded-lg p-4 ${
|
className={`rounded-md p-5 ${
|
||||||
testResult.success
|
testResult.success
|
||||||
? 'bg-green-50 text-green-700 dark:bg-green-900 dark:text-green-300'
|
? 'bg-green-50 text-green-700 dark:bg-green-900/40 dark:text-green-300'
|
||||||
: 'bg-red-50 text-red-700 dark:bg-red-900 dark:text-red-300'
|
: 'bg-red-50 text-red-700 dark:bg-red-900 dark:text-red-300'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{testResult.message}
|
{testResult.message}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{errors.general && (
|
{errors.general && (
|
||||||
<div className="rounded-lg bg-red-50 p-4 text-red-700 dark:bg-red-900 dark:text-red-300">
|
<div className="rounded-2xl bg-red-50 p-5 text-red-700 dark:bg-red-900 dark:text-red-300">
|
||||||
{errors.general}
|
{errors.general}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-between gap-4 px-6 py-4">
|
<div className="px-6 py-2">
|
||||||
<button
|
<div className="flex flex-col gap-4 sm:flex-row sm:justify-between">
|
||||||
onClick={testConnection}
|
|
||||||
disabled={testing}
|
|
||||||
className="flex items-center justify-center rounded-lg border border-gray-300 px-4 py-2 text-gray-700 hover:bg-gray-50 disabled:opacity-50 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700"
|
|
||||||
>
|
|
||||||
{testing ? (
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Spinner />
|
|
||||||
<span className="ml-2">
|
|
||||||
{t('settings.tools.mcp.testing')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
t('settings.tools.mcp.testConnection')
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={testConnection}
|
||||||
setModalState('INACTIVE');
|
disabled={testing}
|
||||||
resetForm();
|
className="border-silver dark:border-dim-gray dark:text-light-gray w-full rounded-3xl border px-6 py-2 text-sm font-medium transition-all hover:bg-gray-100 disabled:opacity-50 sm:w-auto dark:hover:bg-[#767183]/50"
|
||||||
}}
|
|
||||||
className="px-4 py-2 text-gray-600 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200"
|
|
||||||
>
|
>
|
||||||
{t('settings.tools.mcp.cancel')}
|
{testing ? (
|
||||||
</button>
|
<div className="flex items-center justify-center">
|
||||||
<button
|
<Spinner size="small" />
|
||||||
onClick={handleSave}
|
|
||||||
disabled={loading}
|
|
||||||
className="bg-purple-30 hover:bg-violets-are-blue flex items-center justify-center rounded-lg px-6 py-2 text-white disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{loading ? (
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Spinner />
|
|
||||||
<span className="ml-2">
|
<span className="ml-2">
|
||||||
{t('settings.tools.mcp.saving')}
|
{t('settings.tools.mcp.testing')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
t('settings.tools.mcp.save')
|
t('settings.tools.mcp.testConnection')
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<div className="flex flex-col-reverse gap-3 sm:flex-row sm:gap-3">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setModalState('INACTIVE');
|
||||||
|
resetForm();
|
||||||
|
}}
|
||||||
|
className="dark:text-light-gray w-full cursor-pointer rounded-3xl px-6 py-2 text-sm font-medium hover:bg-gray-100 sm:w-auto dark:bg-transparent dark:hover:bg-[#767183]/50"
|
||||||
|
>
|
||||||
|
{t('settings.tools.mcp.cancel')}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={loading}
|
||||||
|
className="bg-purple-30 hover:bg-violets-are-blue w-full rounded-3xl px-6 py-2 text-sm font-medium text-white transition-all disabled:opacity-50 sm:w-auto"
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<Spinner size="small" />
|
||||||
|
<span className="ml-2">
|
||||||
|
{t('settings.tools.mcp.saving')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
t('settings.tools.mcp.save')
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user