feat: finalize remote mcp

This commit is contained in:
Siddhant Rai
2025-09-04 15:10:12 +05:30
parent 7c23f43c63
commit 1bf6af6eeb
11 changed files with 453 additions and 646 deletions

View File

@@ -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,
},
}

View File

@@ -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):

View File

@@ -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,
) )

View File

@@ -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")

View File

@@ -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

View File

@@ -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]

View File

@@ -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

View File

@@ -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',

View File

@@ -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;

View File

@@ -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",

View File

@@ -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>