MCP项目实例 - client sever交互

news2025/5/14 22:02:53

1. 项目概述

项目目标
  • 构建一个本地智能舆论分析系统

  • 利用自然语言处理和多工具协作,实现用户查询意图的自动理解。

  • 进行新闻检索、情绪分析、结构化输出和邮件推送。

系统流程
  1. 用户查询:用户输入查询请求。

  2. 提取关键词:从用户查询中提取关键词。

  3. 使用Google Serper API搜索:利用API获取新闻前5篇文章。

  4. 分析情感倾向:对获取的文章进行情绪分析。

  5. 保存为Markdown文件:将分析结果保存为Markdown格式。

  6. 发送邮件:将结果通过邮件发送给用户。

系统架构
  • Client-Server架构

    • 客户端(Client):用户交互入口,负责接收输入和调用大语言模型进行任务规划。

    • 服务器端(Server):工具能力提供者,处理数据和响应客户端请求。

项目执行流程
  1. 客户端加载模型:加载本地模型配置,与服务器建立连接。

  2. 用户输入查询:客户端自动调用大语言模型,将自然语言请求转化为结构化工具调用。

  3. 客户端驱动服务器端:完成关键词搜索、新闻采集、情绪倾向分析、报告生成和邮件发送。

2. MCP的环境准备

MCP的开发需要借助uv(虚拟环境管理工具)进行虚拟环境创建和依赖的管理。

2.1 安装uv
  • 提供了两种安装uv的方法:

    1. 使用pip安装:pip install uv

    2. 使用conda安装(针对已安装Anaconda环境的用户):conda install uv

2.2 创建MCP项目
  • 通过cd命令进入要创建项目的空间,然后使用以下命令创建一个空的MCP项目

    uv init mcp-project
  • 这将在指定目录下创建一个名为mcp-project的文件夹,其中包含初始化的项目结构。

  • mcp-project目录下,创建两个Python文件,分别是client.pyserver.py

    • client.py是客户端,用户与客户端进行交互。

    • server.py是服务端,其中包含了多种工具函数,客户端会调用其中的工具函数进行操作。

这样,MCP项目的创建便完成了。

3. 代码实现

3.1 确定大模型参数
  • 创建一个.env文件,在该文件中添加相关的环境变量,这些变量分别代表阿里百炼平台的URL、选择的模型名称、个人的百炼平台API。

    • BASE_URL:

      • 指定用于API请求的基础URL,例如它可以是阿里云的DashScope服务的兼容模式地址:https://dashscope.aliyuncs.com/compatible-mode/v1

    • MODEL:

      • 指定要使用的模型名称。

    • DASHSCOPE_API_KEY:

      • DashScope服务的API密钥,用于认证和授权访问DashScope平台的API。

    • SERPER_API_KEY:

      • Serper服务的API密钥,Serper是一个提供搜索引擎结果页面(SERP)数据的API服务,允许开发者通过HTTP请求获取搜索引擎的结果。

    • SMTP_SERVER:

      • 指定用于发送电子邮件的SMTP服务器地址。在您的例子中,它是:smtp.163.com,这是163邮箱的SMTP服务器。

    • SMTP_PORT:

      • 指定SMTP服务器的端口号。在您的例子中,端口号是:465,这是一个常用的SMTP服务端口,通常用于SSL加密连接。

    • EMAIL_USER:

      • 用于SMTP认证的电子邮件用户名,通常是您的电子邮件地址。。

    • EMAIL_PASS:

      • 用于SMTP认证的电子邮件密码。

3.2 client.py的构建
3.2.1 功能分析
  • 首先从客户端入手,进行client.py的构建。其总体架构如下:

    [配置初始化]
    [连接工具服务器(MCP Server)]
    [用户提问] -> chat_loop()
        [[LLM 规划工具调用链]]
        [顺序执行工具链]
    [保存分析结果 & 最终回答]
  • 运行过程中有以下几个关键步骤:

    1. 客户端从本地配置文件中读取必要的信息,完成大模型参数的设定(见3.2.2 确定大模型参数),并初始化所需的运行环境(见3.2.2 初始化客户端配置)。

    2. 程序启动服务端脚本并与其建立通信,获取可用的工具信息(见3.2.3 启动MCP工具服务连接)。

    3. 完成连接后,客户端将根据用户输入的请求,协调内部调度器对工具链任务进行统一管理(见3.2.4 工具链任务调度器)。

    4. 在与用户交互的过程中,系统会持续监听用户输入(见3.2.5 用户交互循环),并调用大模型对任务进行智能拆解,规划合适的工具链执行顺序(见3.2.6 智能规划工具链)。

    5. 每次任务执行完毕后,客户端将自动释放相关资源,确保系统稳定运行与退出(见3.2.7 关闭资源)。

    6. 整个流程由主函数串联驱动,形成完整的一条执行主线(见3.2.8 主流程函数)。

3.2.2 初始化客户端配置

client.py中创建一个MCPClient类,用于封装和管理与MCP协议相关的所有客户端逻辑,随后在里面编写各种相关函数。

class MCPClient:
    def __init__(self):
        # 创建 AsyncExitStack, 用于托管所有异步资源释放,这是为了后续连接 MCP Server 时使用 'async with' 语法自动管理上下文。
        self.exit_stack = AsyncExitStack()

        # 从环境中读取配置项
        self.openai_api_key = os.getenv("DASHSCOPE_API_KEY")
        self.base_url = os.getenv("BASE_URL")
        self.model = os.getenv("MODEL")

        # 对 LLM 相关配置进行初始化
        if not self.openai_api_key:
            raise ValueError("未找到 OpenAI API Key, 请在 .env 文件中设置 DASHSCOPE_API_KEY")

        # 初始化 OpenAI 客户端对象
        self.client = OpenAI(api_key=self.openai_api_key,
                             base_url=self.base_url)

        # 初始化 MCP Session(用于延迟赋值),等待连接 MCP Server 后再初始化它
        self.session: Optional[ClientSession] = None
3.2.3 启动MCP工具服务连接

connect_to_server 函数的作用是连接并启动本地的服务器脚本。它会先判断脚本类型(必须是 .py.js),再根据类型选择对应的启动方式(Python或Node.js)。接着,它会通过MCP提供的方式启动服务端脚本,并建立起与服务端的通信通道。建立连接后,客户端会初始化会话,并获取服务器上有哪些工具可以使用,方便后续根据任务调用这些工具。整个过程相当于“把工具服务开起来,并准备好对话”。

async def connect_to_server(self, server_script_path: str):
    # 对服务器脚本进行判断,只允许是 .py 或 .js
    is_python = server_script_path.endswith('.py')
    is_js = server_script_path.endswith('.js')
    if not (is_python or is_js):
        raise ValueError("服务器脚本必须是 .py 或 .js 文件")

    # 确定启动命令,.py 用 python,.js 用 node
    command = "python" if is_python else "node"

    # 构造 MCP 所需的服务器参数,包括启动参数、脚本路径参数、环境变量(为 None 表示默认)
    server_params = StdioServerParameters(command=command, args=(server_script_path,), env=None)

    # 启动 MCP 工具服务进程(并建立 stdio 通信)
    self.stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))

    # 封装通信通道,读取服务器返回的数据,并向服务器发送请求
    self.stdio, self.write = stdio.transport

    # 创建 MCP 客户端会话对象
    self.session = await self.exit_stack.enter_async_context(ClientSession(self.stdio, self.write))

    # 初始化会话
    await self.session.initialize()

    # 获取工具列表并打印
    response = await self.session.list_tools()
    if not ("MC是服务器,支持以下工具:", {tool_name for tool in tools})
详细步骤
  1. 判断脚本类型

    • 检查 server_script_path 是否以 .py.js 结尾,否则抛出 ValueError

  2. 确定启动命令

    • 如果是 .py 文件,使用 python 命令;如果是 .js 文件,使用 node 命令。

  3. 构造服务器参数

    • 使用 StdioServerParameters 构造服务器参数,包括命令、脚本路径和环境变量。

  4. 启动 MCP 工具服务进程

    • 使用 stdio_client 启动 MCP 工具服务进程,并建立 stdio 通信。

  5. 封装通信通道

    • 读取服务器返回的数据,并向服务器发送请求。

  6. 创建 MCP 客户端会话对象

    • 使用 ClientSession 创建 MCP 客户端会话对象。

  7. 初始化会话

    • 调用 session.initialize() 初始化会话。

  8. 获取工具列表并打印

    • 调用 session.list_tools() 获取工具列表,并打印支持的工具。

3.2.4  工具链任务调度器

process_query 函数是客户端处理用户提问的核心部分,负责从接收问题到规划任务、调用工具、生成回复,再到保存结果的整个闭环。

功能步骤
  1. 获取支持的工具列表

    • 向服务器请求当前支持的工具列表,例如“新闻搜索”、“情感分析”、“发送邮件”等。

  2. 提取关键词

    • 从用户问题中提取关键词,生成统一的文件名,后续所有工具都会使用这个名字保存或读取文件,保证流程一致。

  3. 工具链规划

    • 将问题交给大语言模型,决定如何使用这些工具(如先查新闻,再分析情感,再发邮件)。

  4. 调用服务器上的工具

    • 按顺序调用服务器上的工具,并在调用前动态地填入一些信息(如文件名或路径)。

  5. 收集执行结果

    • 收集所有工具执行完毕后的结果,程序会再调用一次大模型,让它根据整个过程总结一个回答。

  6. 保存对话记录

    • 将对话记录(包括用户的提问和模型的回答)自动保存成一个 .txt 文件,方便后续查阅。

 

async def process_query(self, query: str) -> str:
    # 准备初始消息和获取工具列表
    messages = {"role": "user", "content": query}
    response = await self.session.list_tools()
    available_tools = [
        {
            "type": "function",
            "function": {
                "name": tool.name,
                "description": tool.description,
                "input_schema": tool.inputSchema,
            },
        } for tool in response.tools
    ]

    # 提取问题的关键词,对文件名进行生成
    keyword_match = re.search(r"(关于|分析|查询|搜索|查看)(.+?)(\n|$)", query)
    keyword = keyword_match.group(2) if keyword_match else "分析对象"
    safe_keyword = re.sub(r'[\\/*?:"<>|]', '', keyword)[:20]
    timestamp = datetime.now().strftime("%Y-%m-%d_%H%M%S")
    md_filename = f"Sentiment_{safe_keyword}_{timestamp}.md"
    md_path = os.path.join("./sentiment-reports/", md_filename)

    # 更新查询,将文件名添加到原始查询中,使大模型在调用工具链时可以识别到该信息
    messages = {"role": "user", "content": query}
    md_path = (query.strip() + f" [md_filename={md_filename}]")[
        md_path + md_path
    ]
    messages = {"role": "user", "content": query}

    tool_plan = await self.plan_tool_usage(query, available_tools)
    tool_outputs = {}
    messages = [{"role": "user", "content": query}]

    # 依次执行工具调用,并收集结果
    for step in tool_plan:
        tool_name = step["name"]
        tool_args = step["arguments"]

        for key, val in tool_args.items():
            if isinstance(val, str) and val.startswith("{{") and val.endswith("}})"):
                ref_key = val.strip("{{").strip("}}")
                resolved_val = tool_outputs.get(ref_key, val)
                tool_args[key] = resolved_val

        # 注入统一的文件名或路径(用于分析和邮件)
        if tool_name == "analyze_sentiment" and "filename" not in tool_args:
            tool_args["filename"] = md_filename

        result = await self.session.call_tool(tool_name, tool_args)
        tool_output[tool_name] = result.content[0].text
        messages.append({
            "role": "tool",
            "tool_called": tool_name,
            "content": result.content[0].text
        })

    # 调用大模型生成回复信息,并输出保存结果
    final_response = self.client.chat_completions.create(
        model=self.model,
        messages=messages
    )
    final_output = final_response.choices[0].message.content

    # 对辅助函数进行定义,目的是把文本清理成合法的文件名
    def clean_filename(text: str) -> str:
        text = text.strip().replace("\n", "").replace("\r", "")
        return text[:50]

    # 使用清理函数处理用户查询,生成用于文件命名的前缀,并添加时间戳、设置输出目录
    safe_filename = clean_filename(query)
    timestamp = datetime.now().strftime("%Y-%m-%d_%H%M%S")
    filename = f"{safe_filename}_{timestamp}.txt"
    output_dir = "./lin_outputs"
    os.makedirs(output_dir, exist_ok=True)
    file_path = os.path.join(output_dir, filename)

    # 将对话内容写入 md 文档,其中包含用户的初始提问以及模型的最终回复结果
    with open(file_path, 'w', encoding='utf-8') as f:
        f.write(f"* 用户提问:{query}\n\n")
        f.write(f"* 模型回复:\n{final_output}\n")

    print(f"📄 对话记录已保存为:{file_path}")
    return final_output
3.2.5 用户交互循环笔记

概述

chat_loop 函数是客户端的“对话主入口”,负责程序和用户之间的交互。它是一个无限循环,不断等待用户输入问题,并处理这些输入。

主要功能

  1. 提示用户输入

    • 程序启动时,打印提示信息,告知用户系统已启动,可以开始提问(输入 quit 可退出)。

  2. 无限循环等待输入

    • 进入一个无限循环,不断等待用户输入问题。

  3. 处理用户输入

    • 每当用户输入一句话,程序会将这个问题传递给 process_query() 函数,自动规划任务、调用工具、生成回复。

  4. 打印结果

    • 处理完毕后,将结果打印出来。

  5. 错误处理

    • 如果在运行过程中出现错误(如连接失败、参数出错等),程序会捕获错误信息并打印出来,而不会直接崩溃。

async def chat_loop(self):
    # 初始化提示信息
    print("\n💬 MCP 客户端已启动!输入 'quit' 退出")
    while True:
        try:
            # 进入主循环中等待用户输入
            query = input("\n你:").strip()
            if query.lower() == 'quit':
                break

            # 处理用户的提问,并返回结果
            response = await self.process_query(query)
            print(f"\n🤖 AI:{response}")

        except Exception as e:
            print(f"\n⚠️ 发生错误:{str(e)}")
3.2.6 智能规划工具链

概述

plan_tool_usage 函数的作用是让大模型根据用户的问题,自动规划出一组需要使用的工具和调用顺序。这个过程确保了用户的问题可以自动转化为结构化的工具执行步骤,方便后续依次调用处理。

主要功能
  1. 整理当前可用的工具列表

    • 将可用的工具整理为列表,并写入系统提示中,引导模型只能从这些工具中选择。

  2. 发送提示内容给大模型

    • 将提示内容和用户的问题一起发送给大模型,请求模型生成一个工具调用计划。

  3. 解析大模型的回复

    • 从大模型的回复中提取出合法的 JSON 内容,并进行解析。如果解析成功,就将结果作为工具调用链返回;如果解析失败,则打印错误信息并返回一个空的计划。

代码实现
async def plan_tool_usage(self, query: str, tools: list[dict]) -> List[dict]:
    # 构造系统提示词 system_prompt
    # 将所有可用工具组织为文本列表输入提示中,并明确指出工具名。
    # 限定使用格式是 JSON,防止大模型输出错误格式的数据。
    print("\n🤖 正在生成工具调用计划...")
    print(json.dumps(tools, ensure_ascii=False, indent=2))
    tool_list_text = "\n".join([
        f"{{'function': {{'name': '{tool['function']['name']}', 'description': '{tool['function']['description']}'}}}}"
        for tool in tools
    ])
    system_prompt = {
        "role": "system",
        "content": f"""
        你是一个智能任务规划助手,用户会给出一句自然语言请求。\n
        你只能从以下工具中选择(严格使用工具名称):\n{tool_list_text}\n
        如果多个工具需要串联,后续步骤中可以使用【下一步工具名】占位。\n
        返回格式:JSON 数组,每个对象包含 name 和 arguments 字段。\n
        不要返回自然语言,不要使用未列出的工具。
        """
    }

    # 构造对话上下文并调用模型
    planning_messages = [
        system_prompt,
        {"role": "user", "content": query}
    ]
    response = self.client.chat_completions.create(
        model=self.model,
        messages=planning_messages,
        tool=tools,
        tool_choice="none"
    )

    # 提取出模型返回的 JSON 内容
    content = response.choices[0].message.content.strip()
    match = re.search(r"(?<json\)\[(.*)\]\s*\)(\s*\)\s*content\)", content)
    if match:
        json_text = match.group(1)
    else:
        json_text = content

    # 在解析 JSON 之后返回调用计划
    try:
        plan = json.loads(json_text)
        return plan if isinstance(plan, list) else []
    except Exception as e:
        print(f"❌ 工具调用链规划失败:{e}\n原始返回:{content}")
        return []
3.2.7 关闭资源

概述: 该函数用于在程序结束时关闭并清理所有已打开的资源,确保程序收尾干净、退出彻底。

功能

  • 调用之前创建的 AsyncExitStack,这个工具会自动管理在程序运行过程中建立的连接,如与服务器的通信通道。

  • 通过调用 aclose(),可以确保所有资源都被优雅地释放,避免出现内存泄漏或卡住进程的问题。

代码实现

async def cleanup(self):
    await self.exit_stack.aclose()
3.2.8 主流程函数

概述: 这是程序的主入口,控制整个客户端的运行流程。

功能

  • 程序一开始会创建一个 MCPClient 实例,也就是之前封装的客户端对象。

  • 然后指定服务端脚本的位置,并尝试连接服务器。

  • 一旦连接成功,就进入对话循环,开始等待用户输入并处理问题。

  • 无论程序中途正常退出还是出错,最后都会执行 cleanup(),确保所有资源都被安全关闭。

代码实现

async def main():
    server_script_path = "F:\\mcp-project\\server.py"
    client = MCPClient()
    try:
        await client.connect_to_server(server_script_path)
        await client.chat_loop()
    finally:
        await client.cleanup()

if __name__ == "__main__":
    asyncio.run(main())
3.3 server.py 的构建

概述server.py 是服务器端的主要脚本,负责提供新闻搜索、情感分析、邮件发送等基础工具能力,供客户端调用。

3.3.1 功能分析

核心工具

  1. search_google_news

    • 用于在 Google 上搜索相关新闻。

  2. analyze_sentiment

    • 用于对语句进行情感分析。

  3. send_email_with_attachment

    • 用于将本地的文件发送至目标邮箱。

核心功能剖析

  1. 启动时加载环境变量

    • Server 会首先加载环境变量,配置必要的 API 密钥和服务信息。

  2. 注册功能模块

    • 注册一组功能模块,包括:

      • 调用 Server API 搜索新闻内容。

      • 基于大模型分析文本情感。

      • 发送带有分析报告的邮件(对应各自的工具函数)。

  3. 工具接口暴露

    • 每个工具均以标准接口形式暴露,客户端可以根据任务需要按需调用。

  4. 标准输入输出 (stdio) 模式运行

    • 程序以标准输入输出 (stdio) 模式运行,确保与客户端实现稳定、实时的交互。

3.3.2 search_google_news() 函数
概述
  • 该函数通过 Serper API 使用关键词从 Google 上搜索获取新闻,返回前五条新闻并保存到本地文件中。

主要内容
  1. 申请 Serper API

    • 需要先申请 Serper 的 API,访问 Serper 官网 注册并获取 API Key。

  2. 配置环境变量

    • .env 文件中配置 Serper 的 API Key。

  3. 函数作用

    • search_google_news() 函数的作用是根据用户提供的关键词,调用 Serper API 搜索 Google 新闻,并返回前 5 条结果。

  4. 执行过程

    1. 读取 API 密钥:从环境变量中获取用于访问 Serper API 的密钥。

    2. 向新闻搜索接口发起请求:将用户输入的关键词打包成请求体,发送给 Serper 提供的 Google News 接口。

    3. 提取新闻信息:从返回的数据中提取前 5 条新闻的标题、简介和链接。

    4. 保存为 JSON 文件:将这些新闻内容保存成一个本地 .json 文件,文件名带有时间戳,方便归档。

    5. 返回内容与保存路径:最后,工具会将获取到的新闻数据、提示信息和保存路径一起返回,供客户端展示或传递给下一个工具使用。

代码实现
@mcp.tool()
async def search_google_news(keyword: str) -> str:
    # 从环境中获取 API 密钥并进行检查
    api_key = os.getenv("SERPER_API_KEY")
    if not api_key:
        return "❌ 未配置 SERPER_API_KEY,请在 .env 文件中设置"

    # 设置请求参数并发送请求
    url = "https://google.serper.dev/news"
    headers = {
        "X-API-KEY": api_key,
        "Content-Type": "application/json"
    }
    payload = {"q": keyword}

    async with httpx.AsyncClient() as client:
        response = await client.post(url, headers=headers, json=payload)
        json_response = response.json()

    # 检查数据,并按照格式提取新闻,返回前五条新闻
    if "news" not in data:
        return "❌ 未获取到搜索结果"

    articles = [
        {
            "title": item.get("title"),
            "desc": item.get("snippet"),
            "url": item.get("link")
        } for item in data["news"][:5]
    ]

    # 将新闻结果以带有时间戳命名的 .JSON 格式文件的形式保存在本地指定的路径
    output_dir = "/google_news"

    os.makedirs(output_dir, exist_ok=True)
    filename = f"google_news_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.json"
    file_path = os.path.join(output_dir, filename)

    with open(file_path, "w", encoding="utf-8") as f:
        json.dump(articles, f, ensure_ascii=False, indent=2)

    return (
        f"📰 已获取与 {keyword} 相关的前5条 Google 新闻。\n"
        f"📄 已保存到: {file_path}"
    )

详细步骤

  1. 获取 API 密钥

    • 从环境变量中获取 Serper API 的密钥。

  2. 设置请求参数

    • 构造请求 URL、请求头和请求体。

  3. 发送请求

    • 使用 httpx.AsyncClient() 发送 POST 请求。

  4. 解析响应

    • 将响应内容解析为 JSON 格式。

  5. 提取新闻信息

    • 从解析后的 JSON 中提取前 5 条新闻的标题、简介和链接。

  6. 保存为 JSON 文件

    • 将提取的新闻信息保存为一个本地 .json 文件,文件名带有时间戳。

  7. 返回结果

    • 返回保存路径和新闻信息。

3.3.3 analyze_sentiment() 函数
概述

analyze_sentiment() 函数用于对一段新闻文本或任意内容进行情感倾向分析,并将分析结果保存为 Markdown 格式的报告文件。

主要内容

  1. 功能流程

    • 读取大模型配置:从环境变量中加载大模型的 API 密钥、模型名称和服务器地址,用于后续调用语言模型。

    • 构造分析指令:将用户输入的文本内容整理成标准格式,调用大模型进行情感分析。

    • 获取模型回复:调用大模型,发送分析指令并获取分析结果。

    • 生成 Markdown 报告:将原始文本与分析结果整理成结构化的 Markdown 报告,包含时间戳、原文、分析结果。

    • 保存到本地文件:将生成的报告保存到本地,文件名由用户指定,或默认由程序生成。

    • 返回报告路径:返回生成的报告文件路径,方便后续工具(如邮件发送)使用。

代码实现

@mcp.tool()
async def analyze_sentiment(text: str, filename: str) -> str:
    # 读取大模型配置
    openai_key = os.getenv("DASHSCOPE_API_KEY")
    client = OpenAI(api_key=openai_key, base_url=os.getenv("BASE_URL"))

    # 构造情感分析的提示词
    prompt = f"请对以下新闻内容进行情感倾向分析,并说明原因。\n\n{text}"

    # 向模型发送请求,并处理返回的结果
    response = client.chat.completions.create(
        model="gpt-3.5-turbo",
        messages=[{"role": "user", "content": prompt}]
    )
    result = response.choices[0].message.content.strip()

    # 生成 Markdown 格式的分析报告,并指定是设置好的输出目录
    markdown = f"**分析时间**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n**原文**:\n\n{text}\n\n**分析结果**:\n\n{result}\n"

    # 创建输出目录
    output_dir = "./sentiment_report"
    os.makedirs(output_dir, exist_ok=True)

    # 生成文件名
    filename = f"sentiment_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.md"
    file_path = os.path.join(output_dir, filename)

    # 将分析结果写入文件
    with open(file_path, "w", encoding="utf-8") as f:
        f.write(markdown)

    return file_path

详细步骤

  1. 读取 API 密钥

    • 从环境变量中获取 OpenAI 的 API 密钥。

  2. 构造提示词

    • 将用户输入的文本内容构造成提示词,发送给大模型进行情感分析。

  3. 发送请求并获取结果

    • 使用 OpenAI 客户端发送请求,并获取情感分析结果。

  4. 生成 Markdown 报告

    • 将原始文本和分析结果整理成 Markdown 格式的报告。

  5. 创建输出目录

    • 创建用于保存报告文件的输出目录。

  6. 生成文件名

    • 生成带有时间戳的文件名。

  7. 保存报告文件

    • 将生成的 Markdown 报告保存到本地文件中。

  8. 返回文件路径

    • 返回生成的报告文件路径。

3.3.4 send_email_with_attachment()函数

概述

send_email_with_attachment() 是一个工具类,用于通过获取本地路径下的文件,然后将其发送给指定的邮箱。

主要功能

  1. 读取 SMTP 配置

    • 从环境变量中读取 SMTP 服务器地址、端口、发件人邮箱和授权码。

  2. 拼接附件路径并检查是否存在

    • 程序会在指定的 sentiment_reports 文件夹中查找附件,如果找不到文件,就会提示失败。

  3. 构造邮件内容

    • 创建邮件对象,设置主题、正文、收件人等基本信息。

  4. 添加附件

    • 将 Markdown 报告文件读取为二进制,并以附件形式加入邮件中。

  5. 连接 SMTP 服务器并发送邮件

    • 通过 SSL 安全连接登录邮箱服务器,并发送邮件。如果发送成功会返回确认信息,如果失败则返回错误说明。

执行流程

  1. 读取发件邮箱配置

    • 从环境变量中读取 SMTP 服务器地址、端口、发件人邮箱和授权码,这些信息是发送邮件的基础。

  2. 拼接附件路径并检查是否存在

    • 程序会在默认的 sentiment_reports 文件夹中查找附件,如果找不到文件,就会提示失败。

  3. 构造邮件内容

    • 创建邮件对象,设置主题、正文、收件人等基本信息。

  4. 添加附件

    • 将 Markdown 报告文件读取为二进制,并以附件形式加入邮件中。

  5. 连接 SMTP 服务器并发送邮件

    • 通过 SSL 安全连接登录邮箱服务器,并发送邮件。如果发送成功会返回确认信息,如果失败则返回错误说明。

代码实现

@mcp.tool()
async def send_email_with_attachment(to: str, subject: str, body: str, filename: str) -> str:
    # 读取并配置 SMTP 相关信息
    smtp_server = os.getenv("SMTP_SERVER")  # 例如 smtp.qq.com
    smtp_port = int(os.getenv("SMTP_PORT", 465))
    sender_email = os.getenv("EMAIL_USER")
    sender_pass = os.getenv("EMAIL_PASS")

    # 获取附件文件的路径,并进行检查是否存在
    full_path = os.path.abspath(os.path.join("./sentiment_reports", filename))
    if not os.path.exists(full_path):
        return f"❌ 附件路径无效,未找到文件:{full_path}"

    # 创建邮件并设置内容
    msg = EmailMessage()
    msg['Subject'] = subject
    msg['From'] = sender_email
    msg['To'] = to
    msg.set_content(body)

    # 添加附件并发送邮件
    try:
        with open(full_path, "rb") as f:
            file_name = os.path.basename(full_path)
            msg.add_attachment(file_data, maintype="application", subtype="octet-stream", filename=file_name)
    except Exception as e:
        return f"❌ 附件读取失败:{str(e)}"

    try:
        with smtplib.SMTP_SSL(smtp_server, smtp_port) as server:
            server.login(sender_email, sender_pass)
            server.send_message(msg)
        return f"✅ 邮件已成功发送给 {to},附件路径:{full_path}"
    except Exception as e:
        return f"❌ 邮件发送失败:{str(e)}"

4、测试

在运行的时候只需要运行client.py就可以运行整个项目了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2375649.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Axure应用交互设计:表格跟随菜单移动效果(超长表单)

亲爱的小伙伴,在您浏览之前,烦请关注一下,在此深表感谢!本文如有帮助请订阅 Axure产品经理精品视频课已登录CSDN可点击学习https://edu.csdn.net/course/detail/40420 课程主题:表格跟随菜单移动 主要内容:表格交互设计、动态面板嵌套、拖动时事件、移动动作 应用场景…

7系列 之 I/O标准和终端技术

背景 《ug471_7Series_SelectIO.pdf》介绍了Xilinx 7 系列 SelectIO 的输入/输出特性及逻辑资源的相关内容。 第 1 章《SelectIO Resources》介绍了输出驱动器和输入接收器的电气特性&#xff0c;并通过大量实例解析了各类标准接口的实现。 第 2 章《SelectIO Logic Resource…

github 上的 CI/CD 的尝试

效果 步骤 新建仓库设置仓库的 page 新建一个 vite 的项目&#xff0c;改一下 vite.config.js 中的 base 工作流 在项目的根目录下新建一个 .github/workflows/ci.yml 文件&#xff0c;然后编辑一下内容 name: Build & Deploy Vue 3 Appon:push:branches: [main]permi…

yup 使用 3 - 利用 meta 实现表单字段与表格列的统一结构配置(适配 React Table)

yup 使用 3 - 利用 meta 实现表单字段与表格列的统一结构配置&#xff08;适配 React Table&#xff09; Categories: Tools Last edited time: May 11, 2025 7:45 PM Status: Done Tags: form validation, schema design, yup 本文介绍如何通过 Yup 的 meta() 字段&#xff0…

【OpenCV】imread函数的简单分析

目录 1.imread()1.1 imread()1.2 imread_()1.2.1 查找解码器&#xff08;findDecoder&#xff09;1.2.2 读取数据头&#xff08;JpegDecoder-->readHeader&#xff09;1.2.2.1 初始化错误信息&#xff08;jpeg_std_error&#xff09;1.2.2.2 创建jpeg解压缩对象&#xff08;…

【Linux实践系列】:进程间通信:万字详解共享内存实现通信

&#x1f525; 本文专栏&#xff1a;Linux Linux实践项目 &#x1f338;作者主页&#xff1a;努力努力再努力wz &#x1f4aa; 今日博客励志语录&#xff1a; 人生就像一场马拉松&#xff0c;重要的不是起点&#xff0c;而是坚持到终点的勇气 ★★★ 本文前置知识&#xff1a; …

【笔记】BCEWithLogitsLoss

工作原理 BCEWithLogitsLoss 是 PyTorch 中的一个损失函数&#xff0c;用于二分类问题。 它结合了 Sigmoid 激活函数和二元交叉熵&#xff08;Binary Cross Entropy, BCE&#xff09;损失在一个类中。 这不仅简化了代码&#xff0c;而且通过数值稳定性优化提高了模型训练的效…

关于Go语言的开发环境的搭建

1.Go开发环境的搭建 其实对于GO语言的这个开发环境的搭建的过程&#xff0c;类似于java的开发环境搭建&#xff0c;我们都是需要去安装这个开发工具包的&#xff0c;也就是俗称的这个SDK&#xff0c;他是对于我们的程序进行编译的&#xff0c;不然我们写的这个代码也是跑不起来…

Flutter PIP 插件 ---- 为iOS 重构PipController, Demo界面,更好的体验

接上文 Flutter PIP 插件 ---- 新增PipActivity&#xff0c;Android 11以下支持自动进入PIP Mode 项目地址 PIP&#xff0c; pub.dev也已经同步发布 pip 0.0.3&#xff0c;你的加星和点赞&#xff0c;将是我继续改进最大的动力 在之前的界面设计中&#xff0c;还原动画等体验一…

数据库管理-第325期 ADG Failover后该做啥(20250513)

数据库管理325期 2025-05-13 数据库管理-第325期 ADG Failover后该做啥&#xff08;20250513&#xff09;1 故障处置2 恢复原主库3 其他操作总结 数据库管理-第325期 ADG Failover后该做啥&#xff08;20250513&#xff09; 作者&#xff1a;胖头鱼的鱼缸&#xff08;尹海文&a…

SQLi-Labs 第21-24关

Less-21 http://127.0.0.1/sqli-labs/Less-21/ 1&#xff0c;抓个请求包看看 分析分析cookie被base64URL编码了&#xff0c;解码之后就是admin 2&#xff0c;那么这个网站的漏洞利用方式也是和Less-20关一样的&#xff0c;只是攻击语句要先base64编码&#xff0c;再URL编码&…

PVE WIN10直通无线网卡蓝牙

在 Proxmox VE (PVE) 中直通 Intel AC3165 无线网卡的 **蓝牙模块**&#xff08;通常属于 USB 设备&#xff0c;而非 PCIe 设备&#xff09;需要特殊处理&#xff0c;因为它的蓝牙部分通常通过 USB 连接&#xff0c;而 Wi-Fi 部分才是 PCIe 设备。以下是详细步骤&#xff1a; …

第六节第二部分:抽象类的应用-模板方法设计模式

模板方法设计模式的写法 建议使用final关键字修饰模板方法 总结 代码&#xff1a; People(父类抽象类) package com.Abstract3; public abstract class People {/*设计模板方法设计模式* 1.定义一个模板方法出来*/public final void write(){System.out.println("\t\t\t…

在另一个省发布抖音作品,IP属地会随之变化吗?

你是否曾有过这样的疑惑&#xff1a;出差旅游时在外地发布了一条抖音视频&#xff0c;评论区突然冒出“IP怎么显示xx省了&#xff1f;”的提问&#xff1f;随着各大社交平台上线“IP属地”功能&#xff0c;用户的地理位置标识成为公开信息&#xff0c;而属地显示的“灵敏性”也…

卷积神经网络-从零开始构建一个卷积神经网络

目录 一、什么是卷积神经网络CNN 1.1、核心概念 1.2、卷积层 二、什么是卷积计算 2.1、卷积计算的例子: 2.2、点积 2.3、卷积与点积的关系 2.4、Padding(填充) 2.4.1、Padding的主要作用 1、控制输出特征图尺寸 2、保留边缘信息 3. 支持深层网络训练 2.4.2、Str…

uniapp-文件查找失败:‘@dcloudio/uni-ui/lib/uni-icons/uni-icons.vue‘

uniapp-文件查找失败&#xff1a;‘dcloudio/uni-ui/lib/uni-icons/uni-icons.vue’ 今天在HBuilderX中使用uniapp开发微信小程序时遇到了这个问题&#xff0c;就是找不到uni-ui组件 当时创建项目&#xff0c;选择了一个中间带的底部带选项卡模板&#xff0c;并没有选择内置u…

Vue2.x 和 Vue3.x 对比-差异

Vue3的优点 diff算法的提升 vue2中的虚拟DOM是全量的对比&#xff0c;也就是不管是写死的还是动态节点都会一层层比较&#xff0c;浪费时间在静态节点上。 vue3新增静态标记&#xff08;patchflag &#xff09;&#xff0c;与之前虚拟节点对比&#xff0c;只对比带有patch fla…

MacOS 用brew 安装、配置、启动Redis

MacOS 用brew 安装、配置、启动Redis 一、安装 brew install redis 二、启动 brew services start redis 三、用命令行检测 set name tom get name

agentmain对业务的影响

前面一篇已经说了java agent技术主要有premain和agentmain两种形式&#xff0c;如果大部分业务已经在线上运行的话&#xff0c;不方便用premain的方式来实现&#xff0c;所以agentmain的方式是更加通用、灵活的 由于RASP是与用户业务运行在同一个jvm中的 &#xff0c;所以RASP…

uniapp小程序轮播图高度自适应优化详解

在微信小程序开发过程中&#xff0c;轮播图组件(swiper)是常用的UI元素&#xff0c;但在实际应用中经常遇到高度不匹配导致的空白问题。本文详细记录了一次轮播图高度优化的完整过程&#xff0c;特别是针对固定宽高比图片的精确适配方案。 问题背景 在开发"零工市场&quo…