MCP客户端与服务端初使用——让deepseek调用查询天气的mcp来查询天气

MCP客户端与服务端初使用——让deepseek调用查询天气的mcp来查询天气

本系列主要通过调用天气的mcp server查询天气这个例子来学习什么是mcp,以及怎么设计mcp。话不多说,我们开始吧。主要参考的是B站的老哥做的一个教程,我把链接放到这里,大家如果有什么不懂的也可以去看一下。
https://www.bilibili.com/video/BV1NLXCYTEbj?spm_id_from=333.788.videopod.episodes&vd_source=32148098d54c83926572ec0bab6a3b1d

https://blog.ZEEKLOG.net/fufan_LLM/article/details/146377471

最终的效果:让deepseek-v3使用天气查询的工具来查询指定地方的天气情况

在这里插入图片描述

技术介绍

MCP,即Model Context Protocol(模型上下文协议),是由Claude的母公司Anthropic在2024年底推出的一项创新技术协议。在它刚问世时,并未引起太多关注,反响较为平淡。然而,随着今年智能体Agent领域的迅猛发展,MCP逐渐进入大众视野并受到广泛关注。今年2月,Cursor宣布正式支持MCP功能,这无疑为MCP的推广按下了加速键,使其迅速走进了众多开发人员的视野。
从本质上讲,MCP是一种在智能体Agent开发过程中被广泛认可并遵循的规范。它就如同古代秦始皇推行的“书同文、车同轨”政策一样,通过建立统一的标准和规范,能够极大地提升各方在协作过程中的效率。当开发者们都遵循这一协议进行智能体Agent的开发时,沟通成本大幅降低,开发流程更加顺畅,进而显著提高了智能体Agent的整体开发效率。
截至目前,已经有上千种基于MCP的工具应运而生。在如此强大且不断壮大的MCP生态系统推动下,未来有望迎来一个全新的时代,即人人能够便捷地手工打造Manus的时代。
MCP解决的问题,就是让Agent开发中调用工具更加方便.因为Agent的关键,就是希望它除了聊天之外,能够使用外部工具.之前的function calling的思路,就是创建一个外部的function作为中介,一边传递大模型的请求,一边调用外部工具,最终让大模型能够间接的调用外部工具.

在这里插入图片描述


在这里插入图片描述


这个的思路很简单就不多介绍了,主要是function calling有什么问题?function calling最大的问题就在于它的工具量非常大,一个简单的外部函数往往就要上百行的代码,并且,为了让大模型认识这些外部函数,还需要为每个外部函数编写一个json schema格式的功能说明,此外还要精心设计一个提示词模板才能提高function calling响应的准确率.
所以MCP的目标,就是为了简化这一过程.先统一名称.把大模型的运行环境成为MCP Clinet,也就是MCP客户端,同时,把外部函数运行环境作为MCP Server,MCP服务端.

在这里插入图片描述


然后,统一客户端和服务端的运行规范.并且要求MCP客户端和服务端之间,也统一按照某个既定的提示词模板进行通信.

这样做目前最大的好处就在于MCP服务器的开发成为一个特定的模板,并且只要开发了一个MCP server之后,大家也就都能够直接进行使用.比如查询天气,网页爬取,查询本地SQL等等通用需求.
于是github中的MCP成堆出现,大模型能够使用的工具不断增多,

在这里插入图片描述


b站的九天老师这块讲的非常清楚,关于agent的入门学习理论是很合适的:
https://www.bilibili.com/video/BV1CcBJYtEne/?vd_source=32148098d54c83926572ec0bab6a3b1d

在这里插入图片描述


这里就不再赘述了,主要包含的四个模块及其作用方面.

在这里插入图片描述

环境构建

我看大模型使用的最多的环境是uv的虚拟环境,这里稍微讲一下他们之间的关系.conda是一个跨语言的包和环境的管理工具(不止有python环境),uv则是一个超快的,主要用于python生态,和pip poetry是一类的

在这里插入图片描述


这里我们选择uv来构建,也借着学习一下uv的安装和命令,首先是通过pip来安装uv,python的版本的话优先考虑py3.11的.

pip install uv 

安装依赖库

uv pip install requests # 与pip install 类似,但是更快

创建虚拟环境.到这里才是真正使用uv创建虚拟环境:

uv venv myenv 

这里不像conda,可以指定自己的python版本,这里默认就是使用当前python版本进行虚拟的.类似于运行了:python -m venv myenv.
然后就可以激活环境:

source myenv/bin/activate 

windows中则是:

myenv\Scripts\activate # Windows

然后在虚拟环境中都要通过uv开头的命令来运行文件,这样能够更加高效.

上面主要是uv的一些基础内容,接下来我们正式建立mcp client和mcp server
创建mcp客户端:
我用的是自己在win上安装的py3.8

uv init mcp-client cd mcp-client 
在这里插入图片描述


然后创建虚拟环境并进入:

uv venv .venv\Scripts\activate 
在这里插入图片描述


接下来安装一些依赖库:

uv add mcp openai python-dotenv 

报错了:

在这里插入图片描述

因为mcp 包要求 Python 版本大于等于 3.10,这就导致依赖解析失败。所以我们推荐的还是py3.11的版本.
那怎么通过uv创建不同的python环境呢?
uv没有这样的功能.所以说我们还是需要一个conda,创建一个py3.11的版本,然后重新直线上述过程.还挺无语的,但是要想要uv的快速原型功能也只好这样了…

conda create -n mcp_server python=3.11 conda activate mcp_server 

然后重新uv:

uv init mcp-client cd mcp-client uv venv .venv\Scripts\activate uv add mcp openai python-dotenv 
在这里插入图片描述


python双重环境…
安装完毕之后:

在这里插入图片描述


创建客户端client.py,是调用大模型的核心部分:

import asyncio import os from openai import OpenAI from dotenv import load_dotenv from contextlib import AsyncExitStack # 加载 .env 文件,确保 API Key 受到保护 load_dotenv() class MCPClient: def __init__(self): """初始化 MCP 客户端""" self.exit_stack = AsyncExitStack() self.openai_api_key = os.getenv("OPENAI_API_KEY")# 读取 OpenAI API Key self.base_url = os.getenv("BASE_URL")# 读取 BASE YRL self.model = os.getenv("MODEL")# 读取 modelif not self.openai_api_key: raise ValueError("❌ 未找到 OpenAI API Key,请在 .env 文件中设置 OPENAI_API_KEY") self.client = OpenAI(api_key=self.openai_api_key, base_url=self.base_url) async def process_query(self, query: str) -> str: """调用 OpenAI API 处理用户查询""" messages =[{"role":"system", "content":"你是一个智能助手,帮助用户回答问题。"}, {"role":"user", "content": query}] try: # 调用 OpenAI API response = await asyncio.get_event_loop().run_in_executor( None, lambda: self.client.chat.completions.create(model=self.model, messages=messages, stream=False ))return response.choices[0].message.content except Exception as e: return f"⚠️ 调用 OpenAI API 时出错: {str(e)}" 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)# 发送用户输入到 OpenAI API print(f"\n🤖 OpenAI: {response}") except Exception as e: print(f"\n⚠️ 发生错误: {str(e)}") async def cleanup(self): """清理资源""" await self.exit_stack.aclose() async def main(): client = MCPClient() try: await client.chat_loop() finally: await client.cleanup()if __name__ =="__main__": asyncio.run(main())

然后在文件夹中新建.env文件,写入对应的api-key:
我测试过了,deepseek官网的api才支持后续的工具的使用,或者是昂贵的openai和claude,这里就不考虑了

BASE_URL=https://api.deepseek.com MODEL=deepseek-chat OPENAI_API_KEY="sk-xxx"

运行:

uv run client.py 
在这里插入图片描述


然后是服务端的核心部分,我们以天气调用的工具为例:
首先你需要一个openweather的API来查询天气:https://openweathermap.org/api

在这里插入图片描述


注册一个账号之后去生成一个API即可

在这里插入图片描述

服务端的依赖项安装:

uv add mcp httpx 

新建一个server.py:

import json import httpx from typing import Any from mcp.server.fastmcp import FastMCP # 初始化 MCP 服务器 mcp = FastMCP("WeatherServer")# OpenWeather API 配置 OPENWEATHER_API_BASE ="https://api.openweathermap.org/data/2.5/weather" API_KEY ="xxx"# 请替换为你自己的 OpenWeather API Key USER_AGENT ="weather-app/1.0" async def fetch_weather(city: str) -> dict[str, Any]| None: """ 从 OpenWeather API 获取天气信息。 :param city: 城市名称(需使用英文,如 Beijing) :return: 天气数据字典;若出错返回包含 error 信息的字典 """ params ={"q": city, "appid": API_KEY, "units":"metric", "lang":"zh_cn"} headers ={"User-Agent": USER_AGENT} async with httpx.AsyncClient() as client: try: response = await client.get(OPENWEATHER_API_BASE, params=params, headers=headers, timeout=30.0) response.raise_for_status()return response.json()# 返回字典类型 except httpx.HTTPStatusError as e: return{"error": f"HTTP 错误: {e.response.status_code}"} except Exception as e: return{"error": f"请求失败: {str(e)}"} def format_weather(data: dict[str, Any]| str) -> str: """ 将天气数据格式化为易读文本。 :param data: 天气数据(可以是字典或 JSON 字符串) :return: 格式化后的天气信息字符串 """ # 如果传入的是字符串,则先转换为字典if isinstance(data, str): try: data = json.loads(data) except Exception as e: return f"无法解析天气数据: {e}"# 如果数据中包含错误信息,直接返回错误提示if"error"in data: return f"⚠️ {data['error']}"# 提取数据时做容错处理 city = data.get("name", "未知") country = data.get("sys", {}).get("country", "未知") temp = data.get("main", {}).get("temp", "N/A") humidity = data.get("main", {}).get("humidity", "N/A") wind_speed = data.get("wind", {}).get("speed", "N/A")# weather 可能为空列表,因此用 [0] 前先提供默认字典 weather_list = data.get("weather", [{}]) description = weather_list[0].get("description", "未知")return( f"🌍 {city}, {country}\n" f"🌡 温度: {temp}°C\n" f"💧 湿度: {humidity}%\n" f"🌬 风速: {wind_speed} m/s\n" f"🌤 天气: {description}\n") @mcp.tool() async def query_weather(city: str) -> str: """ 输入指定城市的英文名称,返回今日天气查询结果。 :param city: 城市名称(需使用英文) :return: 格式化后的天气信息 """ data = await fetch_weather(city)return format_weather(data)if __name__ =="__main__":# 以标准 I/O 方式运行 MCP 服务器 mcp.run(transport='stdio')

新的客户端部分client_new.py:

import asyncio import os import json from typing import Optional from contextlib import AsyncExitStack from openai import OpenAI from dotenv import load_dotenv from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client # 加载 .env 文件,确保 API Key 受到保护 load_dotenv() class MCPClient: def __init__(self): """初始化 MCP 客户端""" self.exit_stack = AsyncExitStack() self.openai_api_key = os.getenv("OPENAI_API_KEY")# 读取 OpenAI API Key self.base_url = os.getenv("BASE_URL")# 读取 BASE YRL self.model = os.getenv("MODEL")# 读取 modelif not self.openai_api_key: raise ValueError("❌ 未找到 OpenAI API Key,请在 .env 文件中设置 OPENAI_API_KEY") self.client = OpenAI(api_key=self.openai_api_key, base_url=self.base_url)# 创建OpenAI client self.session: Optional[ClientSession]= None self.exit_stack = AsyncExitStack() async def connect_to_server(self, server_script_path: str): """连接到 MCP 服务器并列出可用工具""" 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 文件")command="python"if is_python else"node" server_params = StdioServerParameters(command=command, args=[server_script_path], env=None )# 启动 MCP 服务器并建立通信 stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params)) self.stdio, self.write = stdio_transport self.session = await self.exit_stack.enter_async_context(ClientSession(self.stdio, self.write)) await self.session.initialize()# 列出 MCP 服务器上的工具 response = await self.session.list_tools() tools = response.tools print("\n已连接到服务器,支持以下工具:", [tool.name fortoolin tools]) async def process_query(self, query: str) -> str: """ 使用大模型处理查询并调用可用的 MCP 工具 (Function Calling)""" print(11111) messages =[{"role":"user", "content": query}] print(2222) response = await self.session.list_tools() print(33333) available_tools =[{"type":"function", "function":{"name": tool.name, "description": tool.description, "input_schema": tool.inputSchema }}fortoolin response.tools] print(available_tools) print(44) response = self.client.chat.completions.create(model=self.model, messages=messages, tools=available_tools ) print(5555)# 处理返回的内容 content = response.choices[0]if content.finish_reason =="tool_calls":# 如何是需要使用工具,就解析工具 tool_call = content.message.tool_calls[0] tool_name = tool_call.function.name tool_args = json.loads(tool_call.function.arguments)# 执行工具 result = await self.session.call_tool(tool_name, tool_args) print(f"\n\n[Calling tool {tool_name} with args {tool_args}]\n\n")# 将模型返回的调用哪个工具数据和工具执行完成后的数据都存入messages中 messages.append(content.message.model_dump()) messages.append({"role":"tool", "content": result.content[0].text, "tool_call_id": tool_call.id, })# 将上面的结果再返回给大模型用于生产最终的结果 response = self.client.chat.completions.create(model=self.model, messages=messages, )return response.choices[0].message.content return content.message.content # async def process_query(self, query: str) -> str:# """调用 OpenAI API 处理用户查询"""# messages = [{"role": "system", "content": "你是一个智能助手,帮助用户回答问题。"},# {"role": "user", "content": query}]# try:# # 调用 OpenAI API# response = await asyncio.get_event_loop().run_in_executor(# None,# lambda: self.client.chat.completions.create(# model=self.model,# messages=messages# )# )# return response.choices[0].message.content# except Exception as e:# return f"⚠️ 调用 OpenAI API 时出错: {str(e)}" async def chat_loop(self): """运行交互式聊天循环""" print("\n🤖 MCP 客户端已启动!输入 'quit' 退出")while True: try: query = input("\n你: ").strip()if query.lower()=='quit':break print("chat ing") response = await self.process_query(query)# 发送用户输入到 OpenAI API print(f"\n🤖 OpenAI: {response}") except Exception as e: print(f"\n⚠️ 发生错误: {str(e)}") async def cleanup(self): """清理资源""" await self.exit_stack.aclose() async def main(): if len(sys.argv)<2: print("Usage: python client.py <path_to_server_script>") sys.exit(1) client = MCPClient() try: await client.connect_to_server(sys.argv[1]) await client.chat_loop() finally: await client.cleanup()if __name__ =="__main__":import sys asyncio.run(main())

要么新启动一个终端来运行server部分,需要重新进入虚拟环境.当然,uv也支持同时运行,所以可以直接运行:

uv run client_new.py server.py 
在这里插入图片描述


最终效果:

在这里插入图片描述

Read more

C++日志系统:高效异步日志实现解析

C++日志系统:高效异步日志实现解析

目录 什么是日志 1. 项目介绍 2. 开发环境 3. 核心技术 4. 环境搭建 5. 日志系统介绍 5.1 为什么需要日志系统 5.2 日志系统技术实现 5.2.1 同步写日志 5.2.2 异步写日志 6. 相关技术知识补充 6.1 不定参函数 6.2 设计模式 单例模式 工厂模式 建造者模式 代理模式 7. 日志系统框架设计 7.1 模块划分 7.2 模块关系图 8. 代码设计 8.1

By Ne0inhk
【C++】树状数组的使用、原理、封装类、样例

【C++】树状数组的使用、原理、封装类、样例

前言 本博文代码打包下载 C++算法与数据结构分类汇总 树状数组的英文名称是 ‌Binary Indexed Tree‌,缩写为 ‌BIT‌。我习惯翻译成TreeArr。 最常见的应用 有序集合包括若干整数,求小于x的数量。auto it = s.lower(x) , it - s.begin(),这个时间复杂度是O(n)。 由于查询和插入交替进行,故不能用向量。 树状数组的用途 令原始数组是a,长度为n。 基础操作 一,求前缀和。即 ∑ j : 0 i a [ j ] \sum_{j:0}^ia[j] ∑j:0i a[j]。时间复杂度:O(

By Ne0inhk
【C/C++刷题集】string类(一)

【C/C++刷题集】string类(一)

🫧个人主页:小年糕是糕手 💫个人专栏:《C++》《Linux》《数据结构》《C语言》 🎨你不能左右天气,但你可以改变心情;你不能改变过去,但你可以决定未来! 目录 一、字符串最后一个单词的长度 二、验证回文串 三、字符串中的第一个唯一字符 四、反转字符串 一、字符串最后一个单词的长度 字符串最后一个单词的长度 这里我们看题目有一个注意点就是我们平常使用cin输入时遇到空格会停下来,在例子中我们可以看到他有A B C D,如果我们使用cin在遇到第一个A之后就会报错,所以这里我们要用到另一种输入方式:getline 他并不是一个成员函数,而是输入流的全局函数 getline(istream&, string&)(定义在 <string> 头文件中),作用是从输入流中读取一整行内容,存入 string 对象。 // 基础用法(读整行) getline(

By Ne0inhk
【C++---红黑树】在编程的浩瀚星空中,C++犹如一颗璀璨的星辰,以其独有的韵律和节奏,吟唱着智慧与创造的赞歌。它不仅仅是代码的堆砌,更是思维与艺术的交融,引领着无数追梦者,在数字的世界里翩翩起舞。

【C++---红黑树】在编程的浩瀚星空中,C++犹如一颗璀璨的星辰,以其独有的韵律和节奏,吟唱着智慧与创造的赞歌。它不仅仅是代码的堆砌,更是思维与艺术的交融,引领着无数追梦者,在数字的世界里翩翩起舞。

红黑树实现 * 1. 红⿊树的概念 * 1.1红黑树的规则 * 1.2路径问题 * 1.3 红⿊树如何确保最⻓路径不超过最短路径的2倍的? * 1.4 红⿊树的效率 * 2 红⿊树的实现 * 2.1 红黑树大致结构 * 首先:对于颜色来说,我们可以用枚举实现红和黑 * 其次:对于红黑树的结点,需具备以下结构(假设我们用pair<K,V>类型来实现红黑树): * 最后:在实现红黑树的整体结构 * 2.2 红黑树插入 * 2.2.1 红⿊树树插⼊⼀个值的⼤概过程 * 2.2.2

By Ne0inhk