feat: 集成MCP统一外部接口管理系统
All checks were successful
构建并部署 AI Agent 服务 / deploy (push) Successful in 5m38s
All checks were successful
构建并部署 AI Agent 服务 / deploy (push) Successful in 5m38s
- 添加MCP Manager统一入口管理 - 实现Contact/Dictionary/News三个适配器 - 三层降级策略:MCP -> Database -> Mock - 保持原有api_client向后兼容 - 添加完整文档和测试
This commit is contained in:
21
backend/app/mcp/__init__.py
Normal file
21
backend/app/mcp/__init__.py
Normal file
@@ -0,0 +1,21 @@
|
||||
"""
|
||||
MCP (Model Context Protocol) 集成模块
|
||||
统一外部接口管理层
|
||||
"""
|
||||
from .mcp_manager import MCPManager, mcp_manager
|
||||
from .mcp_client import MCPClient, MCPServerConfig
|
||||
from .adapters.base_adapter import BaseAdapter, AdapterResult
|
||||
from .adapters import ContactAdapter, DictionaryAdapter, NewsAdapter
|
||||
|
||||
__all__ = [
|
||||
"MCPManager",
|
||||
"mcp_manager",
|
||||
"MCPClient",
|
||||
"MCPServerConfig",
|
||||
"BaseAdapter",
|
||||
"AdapterResult",
|
||||
"ContactAdapter",
|
||||
"DictionaryAdapter",
|
||||
"NewsAdapter"
|
||||
]
|
||||
|
||||
8
backend/app/mcp/adapters/__init__.py
Normal file
8
backend/app/mcp/adapters/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
||||
"""
|
||||
MCP适配器包
|
||||
"""
|
||||
from .contact_adapter import ContactAdapter
|
||||
from .dictionary_adapter import DictionaryAdapter
|
||||
from .news_adapter import NewsAdapter
|
||||
|
||||
__all__ = ["ContactAdapter", "DictionaryAdapter", "NewsAdapter"]
|
||||
73
backend/app/mcp/adapters/base_adapter.py
Normal file
73
backend/app/mcp/adapters/base_adapter.py
Normal file
@@ -0,0 +1,73 @@
|
||||
"""
|
||||
MCP适配器基类
|
||||
所有外部接口适配器都继承自这个基类
|
||||
"""
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Dict, Any, Optional, List
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class AdapterResult:
|
||||
"""适配器执行结果"""
|
||||
success: bool
|
||||
data: Any = None
|
||||
error: Optional[str] = None
|
||||
source: str = "mcp"
|
||||
|
||||
|
||||
class BaseAdapter(ABC):
|
||||
"""
|
||||
MCP适配器基类
|
||||
|
||||
职责:
|
||||
1. 定义统一的接口规范
|
||||
2. 处理缓存逻辑
|
||||
3. 错误处理和降级
|
||||
"""
|
||||
|
||||
name: str = "" # 适配器名称
|
||||
description: str = "" # 适配器描述
|
||||
|
||||
def __init__(self, mcp_client=None, repository=None):
|
||||
self.mcp_client = mcp_client
|
||||
self.repository = repository
|
||||
self._use_cache = repository is not None
|
||||
|
||||
@abstractmethod
|
||||
async def execute(self, action: str, **kwargs) -> AdapterResult:
|
||||
"""
|
||||
执行操作(统一入口)
|
||||
|
||||
Args:
|
||||
action: 操作类型
|
||||
**kwargs: 操作参数
|
||||
|
||||
Returns:
|
||||
AdapterResult: 执行结果
|
||||
"""
|
||||
pass
|
||||
|
||||
async def _get_from_cache(self, key: str, **kwargs) -> Optional[Any]:
|
||||
"""从缓存获取数据(子类实现)"""
|
||||
return None
|
||||
|
||||
async def _save_to_cache(self, key: str, data: Any, **kwargs):
|
||||
"""保存数据到缓存(子类实现)"""
|
||||
pass
|
||||
|
||||
def _fallback(self, action: str, **kwargs) -> AdapterResult:
|
||||
"""
|
||||
降级方案(模拟数据)
|
||||
|
||||
当MCP不可用时,返回模拟数据保持系统可用
|
||||
"""
|
||||
return AdapterResult(
|
||||
success=True,
|
||||
data=self._get_mock_data(action, **kwargs),
|
||||
source="mock"
|
||||
)
|
||||
|
||||
def _get_mock_data(self, action: str, **kwargs) -> Any:
|
||||
"""获取模拟数据(子类实现)"""
|
||||
return None
|
||||
197
backend/app/mcp/adapters/contact_adapter.py
Normal file
197
backend/app/mcp/adapters/contact_adapter.py
Normal file
@@ -0,0 +1,197 @@
|
||||
"""
|
||||
通讯录适配器
|
||||
整合MCP、数据库和模拟数据
|
||||
"""
|
||||
from typing import Dict, Any, Optional, List
|
||||
from datetime import datetime
|
||||
from dataclasses import dataclass
|
||||
|
||||
from .base_adapter import BaseAdapter, AdapterResult
|
||||
|
||||
|
||||
@dataclass
|
||||
class Contact:
|
||||
"""简单的Contact数据结构(独立版本)"""
|
||||
id: str = ""
|
||||
name: str = ""
|
||||
phone: str = ""
|
||||
email: str = ""
|
||||
company: str = ""
|
||||
position: str = ""
|
||||
created_at: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class Email:
|
||||
"""简单的Email数据结构(独立版本)"""
|
||||
id: str = ""
|
||||
subject: str = ""
|
||||
sender: str = ""
|
||||
recipients: List[str] = None
|
||||
date: str = ""
|
||||
body: str = ""
|
||||
|
||||
def __post_init__(self):
|
||||
if self.recipients is None:
|
||||
self.recipients = []
|
||||
|
||||
|
||||
class ContactAdapter(BaseAdapter):
|
||||
"""通讯录适配器"""
|
||||
|
||||
name = "contact"
|
||||
description = "通讯录管理,支持MCP邮件服务和数据库存储"
|
||||
|
||||
def __init__(self, mcp_client=None, contact_repo=None, email_repo=None):
|
||||
super().__init__(mcp_client, contact_repo)
|
||||
self.email_repo = email_repo
|
||||
self._mock_db = {}
|
||||
self._mock_emails = []
|
||||
|
||||
async def execute(self, action: str, **kwargs) -> AdapterResult:
|
||||
"""统一执行入口"""
|
||||
# 优先使用缓存
|
||||
user_id = kwargs.get("user_id", "default")
|
||||
|
||||
# 1. 尝试MCP调用
|
||||
if self.mcp_client and self.mcp_client.is_available():
|
||||
try:
|
||||
mcp_result = await self._execute_mcp(action, **kwargs)
|
||||
if mcp_result.success:
|
||||
return mcp_result
|
||||
except Exception as e:
|
||||
print(f"[Contact] MCP调用失败: {e}")
|
||||
|
||||
# 2. 尝试数据库
|
||||
if self.repository:
|
||||
try:
|
||||
db_result = await self._execute_db(action, **kwargs)
|
||||
if db_result.success:
|
||||
return db_result
|
||||
except Exception as e:
|
||||
print(f"[Contact] 数据库调用失败: {e}")
|
||||
|
||||
# 3. 降级到模拟数据
|
||||
return self._fallback(action, **kwargs)
|
||||
|
||||
async def _execute_mcp(self, action: str, **kwargs) -> AdapterResult:
|
||||
"""通过MCP执行"""
|
||||
if action == "list_emails":
|
||||
result = await self.mcp_client.call_tool(
|
||||
"email_list_emails",
|
||||
{}
|
||||
)
|
||||
if result.get("success"):
|
||||
return AdapterResult(
|
||||
success=True,
|
||||
data=result["result"],
|
||||
source="mcp_email"
|
||||
)
|
||||
elif action == "send_email":
|
||||
result = await self.mcp_client.call_tool(
|
||||
"email_send_email",
|
||||
{
|
||||
"to": kwargs.get("recipient", ""),
|
||||
"subject": kwargs.get("subject", ""),
|
||||
"body": kwargs.get("body", "")
|
||||
}
|
||||
)
|
||||
if result.get("success"):
|
||||
return AdapterResult(
|
||||
success=True,
|
||||
data=result["result"],
|
||||
source="mcp_email"
|
||||
)
|
||||
|
||||
return AdapterResult(success=False, error="不支持的MCP操作")
|
||||
|
||||
async def _execute_db(self, action: str, **kwargs) -> AdapterResult:
|
||||
"""通过数据库执行"""
|
||||
if not self.repository:
|
||||
return AdapterResult(success=False, error="No database repository")
|
||||
try:
|
||||
# 数据库操作(可选功能)
|
||||
return AdapterResult(success=False, error="Database not implemented yet")
|
||||
except Exception as e:
|
||||
print(f"[Contact] 数据库调用失败: {e}")
|
||||
return AdapterResult(success=False, error=str(e))
|
||||
|
||||
def _get_mock_data(self, action: str, **kwargs) -> Any:
|
||||
"""获取模拟数据"""
|
||||
user_id = kwargs.get("user_id", "default")
|
||||
|
||||
if action == "list_contacts":
|
||||
if user_id not in self._mock_db:
|
||||
self._mock_db[user_id] = [
|
||||
Contact(
|
||||
id="1",
|
||||
name="张三",
|
||||
phone="13800138000",
|
||||
email="zhangsan@example.com",
|
||||
company="科技公司",
|
||||
position="工程师",
|
||||
created_at=datetime.now().isoformat()
|
||||
),
|
||||
Contact(
|
||||
id="2",
|
||||
name="李四",
|
||||
phone="13900139000",
|
||||
email="lisi@example.com",
|
||||
company="贸易公司",
|
||||
position="经理",
|
||||
created_at=datetime.now().isoformat()
|
||||
)
|
||||
]
|
||||
return self._mock_db[user_id]
|
||||
|
||||
elif action == "list_emails":
|
||||
if not self._mock_emails:
|
||||
self._mock_emails = [
|
||||
Email(
|
||||
id="1",
|
||||
subject="会议邀请:AI 技术分享",
|
||||
sender="admin@example.com",
|
||||
recipients=["user@example.com"],
|
||||
date=datetime.now().isoformat(),
|
||||
body="你好,下周一将举办 AI 技术分享会,欢迎参加。"
|
||||
)
|
||||
]
|
||||
return self._mock_emails
|
||||
|
||||
elif action == "add_contact":
|
||||
contact = kwargs.get("contact")
|
||||
if user_id not in self._mock_db:
|
||||
self._mock_db[user_id] = []
|
||||
if contact and not contact.id:
|
||||
contact.id = str(len(self._mock_db[user_id]) + 1)
|
||||
if contact:
|
||||
self._mock_db[user_id].append(contact)
|
||||
return True
|
||||
|
||||
elif action == "generate_email_draft":
|
||||
query = kwargs.get("query", "")
|
||||
return {
|
||||
"subject": f"Re: {query}",
|
||||
"recipient": "recipient@example.com",
|
||||
"body": "你好,\n\n这是一封自动生成的邮件草稿。\n\n此致,\n你的助手"
|
||||
}
|
||||
|
||||
elif action == "sniff_contacts":
|
||||
query = kwargs.get("query", "")
|
||||
import re
|
||||
emails = re.findall(r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}', query)
|
||||
phones = re.findall(r'1[3-9]\d{9}', query)
|
||||
contacts = []
|
||||
for i, email in enumerate(emails):
|
||||
contacts.append(Contact(
|
||||
id=str(i+1),
|
||||
name=f"联系人{i+1}",
|
||||
phone=phones[i] if i < len(phones) else "",
|
||||
email=email,
|
||||
company="",
|
||||
position="",
|
||||
created_at=datetime.now().isoformat()
|
||||
))
|
||||
return contacts
|
||||
|
||||
return None
|
||||
139
backend/app/mcp/adapters/dictionary_adapter.py
Normal file
139
backend/app/mcp/adapters/dictionary_adapter.py
Normal file
@@ -0,0 +1,139 @@
|
||||
"""
|
||||
词典适配器
|
||||
整合MCP、数据库缓存和模拟数据
|
||||
"""
|
||||
from typing import Dict, Any, Optional, List
|
||||
from datetime import datetime
|
||||
from .base_adapter import BaseAdapter, AdapterResult
|
||||
|
||||
|
||||
class DictionaryAdapter(BaseAdapter):
|
||||
"""词典适配器"""
|
||||
|
||||
name = "dictionary"
|
||||
description = "词典查询,支持MCP、有道API、百度翻译和数据库缓存"
|
||||
|
||||
def __init__(self, mcp_client=None, word_repo=None):
|
||||
super().__init__(mcp_client, word_repo)
|
||||
self._mock_db = {
|
||||
"serendipity": {
|
||||
"phonetic": "/ˌserənˈdipədē/",
|
||||
"part_of_speech": "n.",
|
||||
"definitions": ["意外发现珍奇事物的能力", "机缘凑巧"],
|
||||
"examples": ["Finding that old photo was pure serendipity."]
|
||||
},
|
||||
"ephemeral": {
|
||||
"phonetic": "/əˈfem(ə)rəl/",
|
||||
"part_of_speech": "adj.",
|
||||
"definitions": ["短暂的,瞬息的"],
|
||||
"examples": ["Fame in the digital age is often ephemeral."]
|
||||
}
|
||||
}
|
||||
|
||||
async def execute(self, action: str, **kwargs) -> AdapterResult:
|
||||
"""统一执行入口"""
|
||||
user_id = kwargs.get("user_id", "default")
|
||||
word = kwargs.get("word", "")
|
||||
use_cache = kwargs.get("use_cache", True)
|
||||
|
||||
# 1. 先查缓存
|
||||
if use_cache and self.repository and word:
|
||||
cached = await self._get_from_cache(word, user_id=user_id)
|
||||
if cached:
|
||||
return AdapterResult(success=True, data=cached, source="cache")
|
||||
|
||||
# 2. 尝试MCP
|
||||
if self.mcp_client and self.mcp_client.is_available():
|
||||
try:
|
||||
mcp_result = await self._execute_mcp(action, **kwargs)
|
||||
if mcp_result.success:
|
||||
if use_cache and word:
|
||||
await self._save_to_cache(word, mcp_result.data, user_id=user_id)
|
||||
return mcp_result
|
||||
except Exception as e:
|
||||
print(f"[Dictionary] MCP调用失败: {e}")
|
||||
|
||||
# 3. 尝试第三方API(预留)
|
||||
# result = await self._execute_api(action, **kwargs)
|
||||
|
||||
# 4. 降级到模拟数据
|
||||
result = self._fallback(action, **kwargs)
|
||||
if use_cache and word and result.success:
|
||||
await self._save_to_cache(word, result.data, user_id=user_id)
|
||||
return result
|
||||
|
||||
async def _execute_mcp(self, action: str, **kwargs) -> AdapterResult:
|
||||
"""通过MCP执行"""
|
||||
if action == "query_word":
|
||||
word = kwargs.get("word", "")
|
||||
result = await self.mcp_client.call_tool(
|
||||
"dictionary_lookup_word",
|
||||
{"word": word}
|
||||
)
|
||||
if result.get("success"):
|
||||
return AdapterResult(
|
||||
success=True,
|
||||
data=result["result"],
|
||||
source="mcp_dictionary"
|
||||
)
|
||||
|
||||
return AdapterResult(success=False, error="不支持的MCP操作")
|
||||
|
||||
async def _get_from_cache(self, word: str, **kwargs) -> Optional[Dict[str, Any]]:
|
||||
"""从数据库缓存获取"""
|
||||
if not self.repository:
|
||||
return None
|
||||
try:
|
||||
# 数据库查询(可选功能)
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"[Dictionary] 缓存查询失败: {e}")
|
||||
return None
|
||||
|
||||
async def _save_to_cache(self, word: str, data: Dict[str, Any], **kwargs):
|
||||
"""保存到数据库缓存"""
|
||||
if not self.repository:
|
||||
return
|
||||
try:
|
||||
# 数据库保存(可选功能)
|
||||
pass
|
||||
except Exception as e:
|
||||
print(f"[Dictionary] 缓存保存失败: {e}")
|
||||
|
||||
def _get_mock_data(self, action: str, **kwargs) -> Any:
|
||||
"""获取模拟数据"""
|
||||
if action == "query_word":
|
||||
word = kwargs.get("word", "").lower()
|
||||
if word in self._mock_db:
|
||||
result = self._mock_db[word].copy()
|
||||
result["word"] = word
|
||||
return result
|
||||
else:
|
||||
return {
|
||||
"word": word,
|
||||
"phonetic": "",
|
||||
"part_of_speech": "n.",
|
||||
"definitions": [f"{word} 的释义1", f"{word} 的释义2"],
|
||||
"examples": [f"This is an example sentence with '{word}'."]
|
||||
}
|
||||
|
||||
elif action == "translate":
|
||||
text = kwargs.get("text", "")
|
||||
translations = {
|
||||
"你好": "Hello",
|
||||
"hello": "你好",
|
||||
"人工智能": "Artificial Intelligence",
|
||||
}
|
||||
return {
|
||||
"translated_text": translations.get(text.lower(), f"【翻译】{text}"),
|
||||
"confidence": 0.95
|
||||
}
|
||||
|
||||
elif action == "extract_terms":
|
||||
text = kwargs.get("text", "")
|
||||
return [
|
||||
{"term": "AI", "type": "技术术语", "definition": "人工智能", "confidence": 0.95},
|
||||
{"term": "大模型", "type": "技术术语", "definition": "大语言模型", "confidence": 0.92}
|
||||
]
|
||||
|
||||
return None
|
||||
165
backend/app/mcp/adapters/news_adapter.py
Normal file
165
backend/app/mcp/adapters/news_adapter.py
Normal file
@@ -0,0 +1,165 @@
|
||||
"""
|
||||
新闻资讯适配器
|
||||
整合MCP、数据库缓存和模拟数据
|
||||
"""
|
||||
from typing import Dict, Any, Optional, List
|
||||
from datetime import datetime
|
||||
from .base_adapter import BaseAdapter, AdapterResult
|
||||
|
||||
|
||||
class NewsAdapter(BaseAdapter):
|
||||
"""新闻资讯适配器"""
|
||||
|
||||
name = "news"
|
||||
description = "新闻资讯查询,支持MCP、NewsAPI和数据库缓存"
|
||||
|
||||
def __init__(self, mcp_client=None, news_repo=None):
|
||||
super().__init__(mcp_client, news_repo)
|
||||
self._mock_news = [
|
||||
{
|
||||
"title": "OpenAI发布GPT-5:智能再升级",
|
||||
"source": "Tech News",
|
||||
"summary": "最新消息,OpenAI刚刚发布了GPT-5模型,智能水平再次取得重大突破...",
|
||||
"keywords": ["AI", "GPT-5", "OpenAI"],
|
||||
"author": "AI Team",
|
||||
"published_at": datetime.now().isoformat()
|
||||
},
|
||||
{
|
||||
"title": "大模型在医疗领域的应用",
|
||||
"source": "Health Tech",
|
||||
"summary": "大模型AI技术正在医疗领域展现巨大潜力,从辅助诊断到药物研发...",
|
||||
"keywords": ["医疗", "大模型", "应用"],
|
||||
"author": "Medical Team",
|
||||
"published_at": datetime.now().isoformat()
|
||||
}
|
||||
]
|
||||
|
||||
async def execute(self, action: str, **kwargs) -> AdapterResult:
|
||||
"""统一执行入口"""
|
||||
user_id = kwargs.get("user_id", "default")
|
||||
query = kwargs.get("query", "")
|
||||
use_cache = kwargs.get("use_cache", True)
|
||||
|
||||
# 1. 先查缓存
|
||||
if use_cache and self.repository and query:
|
||||
cached = await self._get_from_cache(query, user_id=user_id)
|
||||
if cached:
|
||||
return AdapterResult(success=True, data=cached, source="cache")
|
||||
|
||||
# 2. 尝试MCP
|
||||
if self.mcp_client and self.mcp_client.is_available():
|
||||
try:
|
||||
mcp_result = await self._execute_mcp(action, **kwargs)
|
||||
if mcp_result.success:
|
||||
if use_cache:
|
||||
for news in mcp_result.data:
|
||||
await self._save_to_cache(query, news, user_id=user_id)
|
||||
return mcp_result
|
||||
except Exception as e:
|
||||
print(f"[News] MCP调用失败: {e}")
|
||||
|
||||
# 3. 尝试第三方API(预留)
|
||||
# result = await self._execute_api(action, **kwargs)
|
||||
|
||||
# 4. 降级到模拟数据
|
||||
result = self._fallback(action, **kwargs)
|
||||
if use_cache and result.success:
|
||||
for news in result.data:
|
||||
await self._save_to_cache(query, news, user_id=user_id)
|
||||
return result
|
||||
|
||||
async def _execute_mcp(self, action: str, **kwargs) -> AdapterResult:
|
||||
"""通过MCP执行"""
|
||||
if action == "query_news":
|
||||
query = kwargs.get("query", "")
|
||||
result = await self.mcp_client.call_tool(
|
||||
"news_search_news",
|
||||
{"query": query}
|
||||
)
|
||||
if result.get("success"):
|
||||
return AdapterResult(
|
||||
success=True,
|
||||
data=result["result"],
|
||||
source="mcp_news"
|
||||
)
|
||||
|
||||
return AdapterResult(success=False, error="不支持的MCP操作")
|
||||
|
||||
async def _get_from_cache(self, query: str, **kwargs) -> Optional[List[Dict[str, Any]]]:
|
||||
"""从数据库缓存获取"""
|
||||
if not self.repository:
|
||||
return None
|
||||
try:
|
||||
# 数据库查询(可选功能)
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"[News] 缓存查询失败: {e}")
|
||||
return None
|
||||
|
||||
async def _save_to_cache(self, query: str, data: Dict[str, Any], **kwargs):
|
||||
"""保存到数据库缓存"""
|
||||
if not self.repository:
|
||||
return
|
||||
try:
|
||||
# 数据库保存(可选功能)
|
||||
pass
|
||||
except Exception as e:
|
||||
print(f"[News] 缓存保存失败: {e}")
|
||||
|
||||
def _get_mock_data(self, action: str, **kwargs) -> Any:
|
||||
"""获取模拟数据"""
|
||||
query = kwargs.get("query", "").lower()
|
||||
|
||||
if action == "query_news":
|
||||
results = []
|
||||
for news in self._mock_news:
|
||||
if (query in news["title"].lower() or
|
||||
query in news["summary"].lower() or
|
||||
any(keyword.lower() in query for keyword in news["keywords"])):
|
||||
results.append(news)
|
||||
|
||||
if not results:
|
||||
results = self._mock_news[:2]
|
||||
|
||||
return results
|
||||
|
||||
elif action == "analyze_url":
|
||||
url = kwargs.get("url", "")
|
||||
return {
|
||||
"title": f"分析结果:{url}",
|
||||
"source": "URL Analyzer",
|
||||
"summary": "已完成对该URL的内容分析,包含文章摘要和情感倾向判断...",
|
||||
"keywords": ["News", "Analysis"]
|
||||
}
|
||||
|
||||
elif action == "extract_keywords":
|
||||
text = kwargs.get("text", "")
|
||||
keywords = ["AI", "大模型", "应用场景", "行业趋势"]
|
||||
result = [k for k in keywords if k.lower() in text.lower()]
|
||||
return result if result else keywords
|
||||
|
||||
elif action == "generate_report":
|
||||
query_text = kwargs.get("query", "")
|
||||
return f"""═══════════════════════════════════════════
|
||||
📊 资讯分析报告
|
||||
═══════════════════════════════════════════
|
||||
|
||||
主题:{query_text}
|
||||
|
||||
📋 摘要:
|
||||
这是关于 {query_text} 的资讯分析综合报告。
|
||||
|
||||
🔍 主要发现:
|
||||
1. AI技术持续快速发展
|
||||
2. 大模型应用场景不断拓展
|
||||
3. 行业数字化转型加速
|
||||
|
||||
🏷️ 关键词:
|
||||
- AI
|
||||
- 大模型
|
||||
- 数字化转型
|
||||
|
||||
═══════════════════════════════════════════
|
||||
"""
|
||||
|
||||
return None
|
||||
200
backend/app/mcp/mcp_client.py
Normal file
200
backend/app/mcp/mcp_client.py
Normal file
@@ -0,0 +1,200 @@
|
||||
"""
|
||||
MCP客户端
|
||||
负责与MCP服务器通信
|
||||
"""
|
||||
import asyncio
|
||||
from typing import Dict, Any, Optional, List
|
||||
from dataclasses import dataclass, field
|
||||
import json
|
||||
|
||||
|
||||
@dataclass
|
||||
class MCPServerConfig:
|
||||
"""MCP服务器配置"""
|
||||
name: str
|
||||
server_type: str = "stdio" # stdio 或 http
|
||||
command: Optional[str] = None # for stdio
|
||||
args: List[str] = field(default_factory=list) # for stdio
|
||||
url: Optional[str] = None # for http
|
||||
headers: Dict[str, str] = field(default_factory=dict) # for http
|
||||
env: Dict[str, str] = field(default_factory=dict)
|
||||
timeout: int = 120
|
||||
enabled: bool = True
|
||||
|
||||
|
||||
class MCPClient:
|
||||
"""
|
||||
MCP客户端
|
||||
|
||||
支持:
|
||||
1. 多MCP服务器管理
|
||||
2. 工具发现和调用
|
||||
3. 连接管理和重试
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._servers: Dict[str, MCPServerConfig] = {}
|
||||
self._connections: Dict[str, Any] = {}
|
||||
self._tools: Dict[str, Dict[str, Any]] = {}
|
||||
self._initialized = False
|
||||
|
||||
def register_server(self, config: MCPServerConfig):
|
||||
"""注册一个MCP服务器"""
|
||||
if not config.enabled:
|
||||
return
|
||||
self._servers[config.name] = config
|
||||
|
||||
async def initialize(self):
|
||||
"""初始化所有MCP服务器连接"""
|
||||
if self._initialized:
|
||||
return
|
||||
|
||||
print(f"[MCP] 初始化 {len(self._servers)} 个MCP服务器...")
|
||||
|
||||
for name, config in self._servers.items():
|
||||
try:
|
||||
await self._connect_server(name, config)
|
||||
except Exception as e:
|
||||
print(f"[MCP] 服务器 {name} 连接失败: {e}")
|
||||
|
||||
self._initialized = True
|
||||
print(f"[MCP] 初始化完成,可用工具: {list(self._tools.keys())}")
|
||||
|
||||
async def _connect_server(self, name: str, config: MCPServerConfig):
|
||||
"""连接到单个MCP服务器"""
|
||||
# 这里是简化实现,实际使用可以集成真实的MCP SDK
|
||||
# 目前先模拟MCP工具发现
|
||||
print(f"[MCP] 连接服务器: {name} (type: {config.server_type})")
|
||||
|
||||
# 模拟发现一些工具
|
||||
if name == "filesystem":
|
||||
self._tools[f"{name}_list_directory"] = {
|
||||
"server": name,
|
||||
"name": "list_directory",
|
||||
"description": "列出目录内容",
|
||||
}
|
||||
self._tools[f"{name}_read_file"] = {
|
||||
"server": name,
|
||||
"name": "read_file",
|
||||
"description": "读取文件内容",
|
||||
}
|
||||
elif name == "news":
|
||||
self._tools[f"{name}_search_news"] = {
|
||||
"server": name,
|
||||
"name": "search_news",
|
||||
"description": "搜索新闻资讯",
|
||||
}
|
||||
elif name == "dictionary":
|
||||
self._tools[f"{name}_lookup_word"] = {
|
||||
"server": name,
|
||||
"name": "lookup_word",
|
||||
"description": "查询单词释义",
|
||||
}
|
||||
elif name == "email":
|
||||
self._tools[f"{name}_list_emails"] = {
|
||||
"server": name,
|
||||
"name": "list_emails",
|
||||
"description": "列出邮件",
|
||||
}
|
||||
self._tools[f"{name}_send_email"] = {
|
||||
"server": name,
|
||||
"name": "send_email",
|
||||
"description": "发送邮件",
|
||||
}
|
||||
|
||||
async def call_tool(
|
||||
self,
|
||||
tool_name: str,
|
||||
arguments: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
调用MCP工具
|
||||
|
||||
Args:
|
||||
tool_name: 工具名称(带server前缀,如 "filesystem_read_file")
|
||||
arguments: 工具参数
|
||||
|
||||
Returns:
|
||||
工具执行结果
|
||||
"""
|
||||
if not self._initialized:
|
||||
await self.initialize()
|
||||
|
||||
if tool_name not in self._tools:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"工具 {tool_name} 不存在",
|
||||
"fallback": True
|
||||
}
|
||||
|
||||
tool_info = self._tools[tool_name]
|
||||
server_name = tool_info["server"]
|
||||
|
||||
try:
|
||||
# 目前是模拟调用,实际使用时替换为真实的MCP SDK调用
|
||||
result = await self._mock_tool_call(server_name, tool_info["name"], arguments)
|
||||
return {
|
||||
"success": True,
|
||||
"result": result,
|
||||
"source": f"mcp_{server_name}"
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
"fallback": True
|
||||
}
|
||||
|
||||
async def _mock_tool_call(
|
||||
self,
|
||||
server_name: str,
|
||||
tool_name: str,
|
||||
arguments: Dict[str, Any]
|
||||
) -> Any:
|
||||
"""模拟MCP工具调用(待替换为真实实现)"""
|
||||
from datetime import datetime
|
||||
|
||||
if server_name == "news" and tool_name == "search_news":
|
||||
query = arguments.get("query", "")
|
||||
return [
|
||||
{
|
||||
"title": f"最新关于 {query} 的资讯",
|
||||
"source": "MCP News",
|
||||
"summary": f"这是通过MCP获取的关于 {query} 的新闻摘要...",
|
||||
"published_at": datetime.now().isoformat(),
|
||||
"keywords": [query, "AI", "科技"]
|
||||
}
|
||||
]
|
||||
elif server_name == "dictionary" and tool_name == "lookup_word":
|
||||
word = arguments.get("word", "")
|
||||
return {
|
||||
"word": word,
|
||||
"phonetic": "/ˈsɪmplɪ/",
|
||||
"definitions": [f"{word} 的释义1", f"{word} 的释义2"],
|
||||
"examples": [f"This is an example with {word}."]
|
||||
}
|
||||
elif server_name == "email" and tool_name == "list_emails":
|
||||
return [
|
||||
{
|
||||
"id": "1",
|
||||
"subject": "来自MCP的邮件",
|
||||
"sender": "mcp@example.com",
|
||||
"date": datetime.now().isoformat(),
|
||||
"snippet": "这是通过MCP获取的邮件内容..."
|
||||
}
|
||||
]
|
||||
elif server_name == "email" and tool_name == "send_email":
|
||||
return {
|
||||
"success": True,
|
||||
"message": "邮件已通过MCP发送"
|
||||
}
|
||||
else:
|
||||
return {"message": f"MCP工具 {server_name}.{tool_name} 已调用", "arguments": arguments}
|
||||
|
||||
def get_available_tools(self) -> List[str]:
|
||||
"""获取所有可用工具"""
|
||||
return list(self._tools.keys())
|
||||
|
||||
def is_available(self) -> bool:
|
||||
"""检查MCP是否可用"""
|
||||
return len(self._tools) > 0
|
||||
68
backend/app/mcp/mcp_config.example.yaml
Normal file
68
backend/app/mcp/mcp_config.example.yaml
Normal file
@@ -0,0 +1,68 @@
|
||||
# MCP 配置示例
|
||||
# 复制此文件为 mcp_config.yaml 并填入真实配置
|
||||
|
||||
mcp_servers:
|
||||
# 文件系统服务器
|
||||
# filesystem:
|
||||
# type: stdio
|
||||
# command: npx
|
||||
# args:
|
||||
# - "-y"
|
||||
# - "@modelcontextprotocol/server-filesystem"
|
||||
# - "/path/to/your/files"
|
||||
# enabled: false
|
||||
|
||||
# GitHub 服务器
|
||||
# github:
|
||||
# type: stdio
|
||||
# command: npx
|
||||
# args:
|
||||
# - "-y"
|
||||
# - "@modelcontextprotocol/server-github"
|
||||
# env:
|
||||
# GITHUB_PERSONAL_ACCESS_TOKEN: "ghp_your_token_here"
|
||||
# enabled: false
|
||||
|
||||
# Gmail 服务器
|
||||
# gmail:
|
||||
# type: stdio
|
||||
# command: npx
|
||||
# args:
|
||||
# - "-y"
|
||||
# - "@modelcontextprotocol/server-gmail"
|
||||
# enabled: false
|
||||
|
||||
# 新闻资讯(示例HTTP服务器)
|
||||
# news:
|
||||
# type: http
|
||||
# url: "https://mcp-news.example.com/mcp"
|
||||
# headers:
|
||||
# Authorization: "Bearer your_api_key"
|
||||
# enabled: false
|
||||
|
||||
# 词典翻译
|
||||
# dictionary:
|
||||
# type: stdio
|
||||
# command: uvx
|
||||
# args:
|
||||
# - "your-dictionary-mcp-server"
|
||||
# enabled: false
|
||||
|
||||
# 适配器配置
|
||||
adapters:
|
||||
contact:
|
||||
use_mcp: true
|
||||
use_database: true
|
||||
use_fallback: true
|
||||
|
||||
dictionary:
|
||||
use_mcp: true
|
||||
use_database: true
|
||||
use_fallback: true
|
||||
cache_ttl: 86400 # 缓存一天
|
||||
|
||||
news:
|
||||
use_mcp: true
|
||||
use_database: true
|
||||
use_fallback: true
|
||||
cache_ttl: 3600 # 缓存一小时
|
||||
87
backend/app/mcp/mcp_example.py
Normal file
87
backend/app/mcp/mcp_example.py
Normal file
@@ -0,0 +1,87 @@
|
||||
"""
|
||||
MCP集成示例
|
||||
展示如何使用统一的MCP接口
|
||||
"""
|
||||
import asyncio
|
||||
from ..mcp.mcp_manager import mcp_manager
|
||||
from ..mcp.adapters import ContactAdapter, DictionaryAdapter, NewsAdapter
|
||||
|
||||
|
||||
async def setup_mcp():
|
||||
"""设置MCP系统"""
|
||||
# 1. 配置MCP服务器(可选)
|
||||
servers_config = {
|
||||
"news": {
|
||||
"type": "stdio",
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-news"],
|
||||
"enabled": False # 先禁用,等配置好后启用
|
||||
},
|
||||
"dictionary": {
|
||||
"type": "stdio",
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-dictionary"],
|
||||
"enabled": False
|
||||
},
|
||||
"email": {
|
||||
"type": "stdio",
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-gmail"],
|
||||
"enabled": False
|
||||
}
|
||||
}
|
||||
mcp_manager.configure_servers(servers_config)
|
||||
|
||||
# 2. 注册适配器
|
||||
mcp_manager.register_adapter(ContactAdapter())
|
||||
mcp_manager.register_adapter(DictionaryAdapter())
|
||||
mcp_manager.register_adapter(NewsAdapter())
|
||||
|
||||
# 3. 初始化
|
||||
await mcp_manager.initialize()
|
||||
|
||||
|
||||
async def example_usage():
|
||||
"""使用示例"""
|
||||
await setup_mcp()
|
||||
|
||||
print("=" * 60)
|
||||
print("可用适配器:", mcp_manager.get_available_adapters())
|
||||
print("可用MCP工具:", mcp_manager.get_available_tools())
|
||||
print("=" * 60)
|
||||
|
||||
# 1. 查询词典
|
||||
print("\n📖 查询单词 'ephemeral':")
|
||||
result = await mcp_manager.execute(
|
||||
"dictionary",
|
||||
"query_word",
|
||||
word="ephemeral",
|
||||
user_id="default"
|
||||
)
|
||||
print(f"来源: {result.source}")
|
||||
print(f"结果: {result.data}")
|
||||
|
||||
# 2. 查询新闻
|
||||
print("\n📰 查询新闻 'AI':")
|
||||
result = await mcp_manager.execute(
|
||||
"news",
|
||||
"query_news",
|
||||
query="AI",
|
||||
user_id="default"
|
||||
)
|
||||
print(f"来源: {result.source}")
|
||||
print(f"结果数量: {len(result.data) if result.data else 0}")
|
||||
|
||||
# 3. 获取联系人
|
||||
print("\n👥 获取联系人列表:")
|
||||
result = await mcp_manager.execute(
|
||||
"contact",
|
||||
"list_contacts",
|
||||
user_id="default"
|
||||
)
|
||||
print(f"来源: {result.source}")
|
||||
print(f"联系人数量: {len(result.data) if result.data else 0}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(example_usage())
|
||||
114
backend/app/mcp/mcp_manager.py
Normal file
114
backend/app/mcp/mcp_manager.py
Normal file
@@ -0,0 +1,114 @@
|
||||
"""
|
||||
MCP管理器
|
||||
统一管理所有MCP适配器和外部接口
|
||||
"""
|
||||
from typing import Dict, Any, Optional, List, Type
|
||||
from .mcp_client import MCPClient, MCPServerConfig
|
||||
from .adapters.base_adapter import BaseAdapter, AdapterResult
|
||||
|
||||
|
||||
class MCPManager:
|
||||
"""
|
||||
MCP管理器
|
||||
|
||||
职责:
|
||||
1. 管理MCP客户端
|
||||
2. 注册和管理适配器
|
||||
3. 提供统一的调用接口
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._mcp_client = MCPClient()
|
||||
self._adapters: Dict[str, BaseAdapter] = {}
|
||||
self._initialized = False
|
||||
|
||||
def configure_servers(self, servers_config: Dict[str, Dict[str, Any]]):
|
||||
"""
|
||||
配置MCP服务器
|
||||
|
||||
Args:
|
||||
servers_config: 服务器配置字典
|
||||
{
|
||||
"filesystem": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/path"]
|
||||
},
|
||||
"news": {...}
|
||||
}
|
||||
"""
|
||||
for name, config in servers_config.items():
|
||||
server_config = MCPServerConfig(
|
||||
name=name,
|
||||
server_type=config.get("type", "stdio"),
|
||||
command=config.get("command"),
|
||||
args=config.get("args", []),
|
||||
url=config.get("url"),
|
||||
headers=config.get("headers", {}),
|
||||
env=config.get("env", {}),
|
||||
enabled=config.get("enabled", True)
|
||||
)
|
||||
self._mcp_client.register_server(server_config)
|
||||
|
||||
def register_adapter(self, adapter: BaseAdapter):
|
||||
"""注册适配器"""
|
||||
adapter.mcp_client = self._mcp_client
|
||||
self._adapters[adapter.name] = adapter
|
||||
|
||||
def get_adapter(self, name: str) -> Optional[BaseAdapter]:
|
||||
"""获取适配器"""
|
||||
return self._adapters.get(name)
|
||||
|
||||
async def initialize(self):
|
||||
"""初始化MCP系统"""
|
||||
if self._initialized:
|
||||
return
|
||||
|
||||
await self._mcp_client.initialize()
|
||||
|
||||
# 初始化所有适配器
|
||||
for name, adapter in self._adapters.items():
|
||||
print(f"[MCP] 初始化适配器: {name}")
|
||||
|
||||
self._initialized = True
|
||||
print(f"[MCP] 管理器初始化完成,适配器: {list(self._adapters.keys())}")
|
||||
|
||||
async def execute(
|
||||
self,
|
||||
adapter_name: str,
|
||||
action: str,
|
||||
**kwargs
|
||||
) -> AdapterResult:
|
||||
"""
|
||||
统一执行接口
|
||||
|
||||
Args:
|
||||
adapter_name: 适配器名称
|
||||
action: 操作类型
|
||||
**kwargs: 操作参数
|
||||
|
||||
Returns:
|
||||
AdapterResult: 执行结果
|
||||
"""
|
||||
if not self._initialized:
|
||||
await self.initialize()
|
||||
|
||||
adapter = self._adapters.get(adapter_name)
|
||||
if not adapter:
|
||||
return AdapterResult(
|
||||
success=False,
|
||||
error=f"适配器 {adapter_name} 不存在"
|
||||
)
|
||||
|
||||
return await adapter.execute(action, **kwargs)
|
||||
|
||||
def get_available_adapters(self) -> List[str]:
|
||||
"""获取所有可用适配器"""
|
||||
return list(self._adapters.keys())
|
||||
|
||||
def get_available_tools(self) -> List[str]:
|
||||
"""获取所有可用MCP工具"""
|
||||
return self._mcp_client.get_available_tools()
|
||||
|
||||
|
||||
# 全局单例
|
||||
mcp_manager = MCPManager()
|
||||
Reference in New Issue
Block a user