- 👏作者简介:大家好,我是爱吃芝士的土豆倪,24届校招生Java选手,很高兴认识大家
- 📕系列专栏:Spring原理、JUC原理、Kafka原理、分布式技术原理、数据库技术、JVM原理、AI应用
- 🔥如果感觉博主的文章还不错的话,请👍三连支持👍一下博主哦
- 🍂博主正在努力完成2025计划中:逆水行舟,不进则退
- 📝联系方式:nhs19990716,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬👀
MCP 是什么
如何去开发属于自己的MCP服务,并使用cursoor进行本地配置,那么MCP是什么?首先我们可以进入到MCP的官方网站
https://mcp-docs.cn/introduction
全称是Model Context Protocol,是连接大模型 与 外部资源工具的标准化接口服务,说的通俗一些呢,MCP就是标准化的function call,只不过这个function call是属于大模型与外部资源工具之间的交互。模型本身呢只是对数据进行计算 和 处理的。
模型本身是不具备获取外部资源 和 工具的能力,而MCP为大模型提供的 具备外部资源 和 工具的能力,并且MCP的诞生可以让大模型自行调用这些资源 和 工具,比如AI模型没有能力使用我们本地数据库的数据,然后使用特定结构的sql查询获取数据再生成图表的这个过程。
那么使用MCP来实现这个功能,一个MCP负责通过SQL查询获取数据,然后另一个MCP负责生成图表,比如生成图片格式或者HTML格式,这些都是可以让MCP提供工具,让大模型去使用的。
那么本篇文章会依据官方文档从零开始使用MCP开发一个天气查询服务。
我们可以继续看下mcp的整体运行流程。
上图是mcp的客户端 和 三个mcp服务在一台服务器上部署,前两个mcp服务对接的是本地的数据库数据,第三个mcp服务通过web api去请求了互联网上的外部资源数据,mcp客户端可以获知这三个mcp server的具体能力,然后再把这些能力交给大模型,那么大模型就会知道这三个mcp server的具体能力,那么在我们使用大模型的时候,大模型就可以自行判断是否要使用相关mcp的tools,也就是大模型的工具,接下来ai模型就可以通过某个mcp server提供的工具获取我哦们本地数据库的数据 或者 外部资源的数据,对相关数据进行计算 和 推理。
关于mcp server的开发,根据官方文档的描述,核心就是我们去创建一个天气查询的mcp服务,主要提供几个tools,然后我们把这几个tools连接到我们的mcp主机上,也就是带有client的大模型中,就可以正常使用了。
MCP 核心概念
接下来说一下mcp的核心概念,其实他可以提供三种主要类型的功能。
第一种resource,客户端可以读取类似文件的数据,如api响应 或 文件内容,也就是说,我们自己开发好的后端api 或 第三方的api接口 或者第三方文件可以通过resource来进行读取。区别在于传统api使用时或者我们程序主动调用 或 处理数据的过程,resource将数据源的注册为标准化URI资源,允许模型通过统一接口直接访问。无需关心底层实现细节。也就是说我们有一个url,这个url可以直接接入数据库某个表的数据,然后模型就可以直接通过这个url来获取这个表的数据,并且不会像使用tools那样需要我们进行确认,这样的做法,丰富了模型的上下文信息。对于resource,标准规范是只对数据进行读取,不能进行写入。
第二个 tools可以让模型进行相关的工具函数的使用,但是他需要用户批准,也就是说我们开发的mcp服务,可以提供很多函数,这些函数可以让模型自行去使用,比如说我们获取了一个获取天气预报的函数,那么模型就可以自己调用这个函数来获取天气信息,那么类比一下,就好比我们传统开发后端api一样,我们自己开发了一个获取天气预报的api接口,然后我们自己去使用这个api来获取天气预报信息,只不过这个过程是把我们自己替换成了模型而已。
第三个 prompts提示词,帮助用户完成特定任务的预写模板,其实就是为tools工具提供了一个提示词,让模型可以根据提示词模板进行回答,就比如说我通过天气预报函数 获取今天天气的同时,我还想让模型给我出门穿衣服的建议,那么我就可以为这个天气函数提供一个提示词,让模型可以根据提示词模板来进行回答。
设置环境
接下来使用python来开发一下这个 server。版本必须在3.10以上、Python MCP SDK 1.2.0 或者更高版本
根据官方文档,简单设置下环境:
curl -LsSf https://astral.sh/uv/install.sh | sh
之后请确保重启终端,以确保 uv 命令被正确识别。
现在,让我们创建并设置我们的项目:
# 为我们的项目创建一个新目录
uv init weather
cd weather
# 创建虚拟环境并激活它,我们创建一个目录 weather_env 来存放虚拟环境
uv venv weather_env
source ./weather_env/Scripts/activate
# 测试 python 是否是指定的当前虚拟环境中的python
which python
# 安装 http 依赖用于调用外部API 和 mcp cli的相关命令行工具,
# --active 让 uv 使用当前激活的环境安装相关依赖
uv add "mcp[cli]" httpx --active
# 创建我们的服务器文件
touch weather.py
其中mcp clie提供了对mcp server进行本地调试的能力,而httpx主要是用于对外部的天气api接口进行请求,因为我们需要http请求的方式。
当前置的准备工具就位后,就可以在 weather.py 文件里开发了。。。
正式构建应用
接下来就开始正式开发了
导入包
from typing import Any
# 用于发送 HTTP 请求
import httpx
# 用于创建 MCP 服务
from mcp.server.fastmcp import FastMCP
# 初始化一个MCP服务实例,服务名称就是weather,这将作为 MCP 客户端或大模型识别服务的标识
mcp = FastMCP("weather")
# 定义OpenWeatherMap API 的基础URL
OPENWEATHER_API_BASE = "https://api.openweathermap.org/data/2.5"
# API 密钥
OPENWEATHER_API_KEY = "××"
# API 调用的身份识别
USER_AGENT = "weather-app/1.0"
关于openWeather没有注册的需要注册下
https://openweathermap.org/api
这里面有一个接口是根据城市名称获取天气信息的api接口。使用之前需要一个api key需要前置自己申请好!!!
辅助函数
# 核心工具函数:负责向OpenWeatherMap API 发送请求并处理响应
async def make_weather_request(url: str) -> dict[str, Any] | None:
"""向OpenWeatherMap API 发送请求并处理响应,包括适当的错误处理。
Args:
url (str): 完整的OpenWeatherMap API 请求URL
Returns:
dict[str, Any]
None: 如果请求成功,返回响应的JSON数据,否则返回None
"""
# 设置请求头,包含User-Agent标识
headers = {
"User-Agent": USER_AGENT,
"Accept": "application/json",
"Content-Type": "application/json",
}
# 创建异步Http客户端会话
async with httpx.AsyncClient() as client:
try:
# 发送GET请求,并设置请求头和超时
response = await client.get(url, headers=headers, timeout=30.0)
# 检查HTTP状态,非2XX状态码会抛出异常
response.raise_for_status()
# 解析JSON响应数据并返回
return response.json()
except Exception as e:
print(f"错误 {e}")
return None
调用的核心工具函数就以及开发完了,它主要是对天气API进行请求,并获取api返回的数据。
# 辅助函数:将当前JSON格式的天气数据转换为可读的文本格式
def format_weather_data(data: dict, units: str = "metric") -> str:
"""将JSON格式的天气数据转换为可读的文本字符串格式
Args:
data (dict): JSON格式的天气数据
Returns:
str: 格式化的天气信息文本
"""
# 检查data是否为空
if not data:
return "无法获取天气数据"
# 从 API 响应中提取关键天气信息
# 获取国家代码
country_code = data.get("sys", {}).get("country", "未知")
# 获取城市名称
city_name = data.get("name", "未知")
# 获取天气描述
weather_desc = data.get("weather", [{}])[0].get("description", "未知")
# 获取当前温度(摄氏度)
temp = data.get("main", {}).get("temp", "未知")
# 获取体感温度(摄氏度)
feels_like = data.get("main", {}).get("feels_like", "未知")
# 获取湿度(百分比)
humidity = data.get("main", {}).get("humidity", "未知")
# 获取风速(m/s)
wind_speed = data.get("wind", {}).get("speed", "未知")
# 根据 units 参数确定温度和风速的单位
temp_unit = "°C" if units == "metric" else "°F"
wind_speed_unit = "m/s" if units == "metric" else "mph"
# 构建格式化的天气信息字符串
return f"""
城市: {city_name}
国家: {country_code}
天气: {weather_desc}
温度: {temp} {temp_unit}
体感温度: {feels_like} {temp_unit}
湿度: {humidity}%
风速: {wind_speed} {wind_speed_unit}
"""
想知道具体的返回格式只需要 curl一下即可,就能拿到具体的返回值,然后直接封装解析即可。
工具开发
基础版本
# @mcp.tool() 是一个装饰器,用于将函数注册为 MCP 工具,允许大模型进行调用
# @mcp.tool(name="get_weather", description="获取指定城市的当前天气信息, 如果用户使用中文询问某城市天气,你必须将城市名转换为相应的英文名称再调用API。")
@mcp.tool()
# MCP 工具1:根据城市获取当前的天气信息,由大模型调用这个函数,函数参数由大模型传入
async def get_weather(city: str, country_code: str = None, state_code: str = None, units: str = "metric", lang: str = "zh_cn") -> str:
"""获取指定城市的当前天气信息,如果用户使用中文询问某城市天气,
你必须将城市名转换为相应的英文名称再调用API。
Args:
city: 城市名称 (例如:Beijing, Shanghai, New York)
country_code: 国家代码(可选,例如:CN, US, JP)
state_code: 州/省代码(可选,例如:BJ, SH, NY)
units: 测量单位(可选,默认metric)
lang: 语言(可选,默认zh_cn)
Returns:
str: 格式化的当前天气信息文本
"""
# 构建位置查询参数,支持城市名称、州代码和国家代码组合
location_query = city
if state_code and country_code:
# 格式:城市,州代码,国家代码
location_query = f"{city},{state_code},{country_code}"
if country_code:
# 格式:城市,国家代码
location_query = f"{city},{country_code}"
# 构建API请求URL,参数包含位置查询、API密钥、测量单位和语言
url = f"{OPENWEATHER_API_BASE}/weather?q={location_query}&appid={OPENWEATHER_API_KEY}&units={units}&lang={lang}"
# 发送请求并获取响应数据
data = await make_weather_request(url)
# 检查 API 响应中的错误代码
if "cod" in data and data["cod"] != 200:
return f"获取天气信息失败,错误:{data.get('message', '未知错误')}"
# 使用辅助函数格式化天气信息并返回,传入data和units参数
return format_weather_data(data, units)
其实上述中的描述是最关键的,就按照这个格式来就好。
"""获取指定城市的当前天气信息,如果用户使用中文询问某城市天气,
你必须将城市名转换为相应的英文名称再调用API。
Args:
city: 城市名称 (例如:Beijing, Shanghai, New York)
country_code: 国家代码(可选,例如:CN, US, JP)
state_code: 州/省代码(可选,例如:BJ, SH, NY)
units: 测量单位(可选,默认metric)
lang: 语言(可选,默认zh_cn)
Returns:
str: 格式化的当前天气信息文本
"""
描述的作用是 大模型最终会根据我们函数中的描述去选择它最终要去调用哪个tool工具
# 程序入口点
if __name__ == "__main__":
# 初始化并运行 MCP 服务,使用标准输入输出作为传输方式
mcp.run(transport='stdio')
我们目前是mcp服务会部署在本地,会使用cursor这个mcp客户端去使用。
接下来我们要怎么测试这个服务呢?
测试运行
mcp dev weather.py
替换路径即可,点击connect即可。
此时我们就连接成功了,然后在右侧输入测试即可。
基本版本2
# MCP 工具2:提供5天天气预报的查询功能,传入城市名称、国家代码(可选)、州代码(可选)、测量单位(可选)和语言(可选)
@mcp.tool()
async def get_forecast(city: str, country_code: str = None, state_code: str = None, units: str = "metric", lang: str = "zh_cn") -> str:
"""获取指定城市的5天天气预报信息,如果用户使用中文询问某城市5天天气预报,
你必须将城市名转换为相应的英文名称再调用API。
Args:
city: 城市名称 (例如:Beijing, Shanghai, New York)
country_code: 国家代码(可选,例如:CN, US, JP)
state_code: 州/省代码(可选,例如:BJ, SH, NY)
units: 测量单位(可选,默认metric)
lang: 语言(可选,默认zh_cn)
Returns:
str: 格式化的5天天气预报信息文本
"""
# 构建位置查询参数,支持城市名称、州代码和国家代码组合
location_query = city
if state_code and country_code:
# 格式:城市,州代码,国家代码
location_query = f"{city},{state_code},{country_code}"
if country_code:
# 格式:城市,国家代码
location_query = f"{city},{country_code}"
# 构建API请求URL,参数包含位置查询、API密钥、测量单位和语言
url = f"{OPENWEATHER_API_BASE}/forecast?q={location_query}&appid={OPENWEATHER_API_KEY}&units={units}&lang={lang}"
# 发送请求并获取响应数据
data = await make_weather_request(url)
# 检查data是否为空
if not data:
return "无法获取天气数据"
# 检查 API 响应中的错误代码
if "cod" in data and data["cod"] != "200":
return f"获取5天天气预报信息失败,错误:{data.get('message', '未知错误')}"
# 提取5天天气预报信息
forecast_list = data.get("list", [])
# 如果天气数组为空
if not forecast_list:
return "无法获取5天天气预报信息"
# 构建格式化的5天天气预报信息数组
forecast_data = []
# 遍历5天天气预报信息,并提取日期和时间
for forecast in forecast_list:
# 提取日期和时间
date_time = forecast.get("dt_txt", "")
# 提取天气描述
weather_desc = forecast.get("weather", [{}])[0].get("description", "未知")
# 提取温度
temp = forecast.get("main", {}).get("temp", "未知")
# 提取湿度
humidity = forecast.get("main", {}).get("humidity", "未知")
# 提取风速
wind_speed = forecast.get("wind", {}).get("speed", "未知")
# 根据 units 参数确定温度和风速的单位
temp_unit = "°C" if units == "metric" else "°F"
wind_speed_unit = "m/s" if units == "metric" else "mph"
# 构建格式化的5天天气预报信息字符串
forecast_str = f"""
日期: {date_time}
天气: {weather_desc}
温度: {temp} {temp_unit}
湿度: {humidity}%
风速: {wind_speed} {wind_speed_unit}
"""
# 提取天气预报数据
forecast_data.append(forecast_str)
# 返回格式化的5天天气预报信息数组,使用分隔符链接所有天气预报
return "\n---\n".join(forecast_data)
测试同上。
cursor运行
接下来给cursor配置mcp server
{
"mcpServers": {
"weather": {
"command": "uv",
"args": [
"--directory",
"/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather",
"run",
"weather.py"
]
}
}
}
含有模板的 版本
# 这个就是告诉大模型你应该使用什么文本形式给我答案
# mcp.prompt() 是一个装饰器,用于将函数注册为 MCP 提示,允许大模型进行调用
@mcp.prompt()
# 那么这个函数接收的数据是由大模型传入的,也就是说它把天气数据传入进来。
async def weather_prompt(city: str, weather_desc: str, temp: float, humidity: float, wind_speed: float, temp_unit: str, speed_unit: str) -> str:
"""用于生成天气报告的提示模板
Args:
city: 城市名称
weather_desc: 天气描述
temp: 温度
humidity: 湿度
wind_speed: 风速
temp_unit: 温度单位
speed_unit: 风速单位
"""
# 构建天气报告的格式化文本
return f"""请你作为专业的气象播报员,根据以下天气数据生成一份简介、易懂的天气报告:
城市: {city}
天气: {weather_desc}
温度: {temp} {temp_unit}
湿度: {humidity}%
风速: {wind_speed} {speed_unit}
报告内容包括:
1.今日天气概况
2.根据温度和湿度分析体感情况
3.根据天气状况提供穿衣建议
4.适合的户外活动推荐
最后请使用自然、专业的语言,避免过于技术性的术语,要贴近生活。
"""
# mcp工具3:获取指定城市的天气信息并提供报告模板
@mcp.tool()
async def weather_report(city: str, country_code: str = None, state_code: str = None, units: str = "metric", lang: str = "zh_cn") -> dict:
"""获取指定城市的天气信息并提供报告模板
Args:
city: 城市名称
country_code: 国家代码(可选,例如:CN, US, JP)
state_code: 州/省代码(可选,例如:BJ, SH, NY)
units: 测量单位(可选,默认metric)
lang: 语言(可选,默认zh_cn)
Returns:
dict: 包含天气数据和提示模板信息的字典
"""
# 获取原始天气信息
weather_result = await get_weather(city, country_code, state_code, units, lang)
# 解析天气文本获取关键信息
import re
# 使用正则表达式提取天气信息
city_match = re.search(r'城市: (.*?)(?:\n|$)', weather_result)
weather_match = re.search(r'天气: (.*?)(?:\n|$)', weather_result)
temp_match = re.search(r'温度: ([\d.]+)(.*?)(?:\n|$)', weather_result)
humidity_match = re.search(r'湿度: ([\d.]+)%', weather_result)
wind_match = re.search(r'风速: ([\d.]+) (.*?)(?:\n|$)', weather_result)
# 提取数据值,如果无法提取则使用默认值
city_name = city_match.group(1) if city_match else city
weather_desc = weather_match.group(1) if weather_match else "未知"
temp_value = float(temp_match.group(1)) if temp_match else 0.0
temp_unit = temp_match.group(2) if temp_match and len(temp_match.groups()) > 1 else "°C"
humidity_value = int(float(humidity_match.group(1))) if humidity_match else 0
wind_speed = float(wind_match.group(1)) if wind_match else 0.0
speed_unit = wind_match.group(2) if wind_match and len(wind_match.groups()) > 1 else "m/s"
# 构建返回结果,包含三个部分:原始的数据,模板名称,模板参数
# 大模型调用这个工具会收到包含原始数据,模板名称,模板参数的结构化返回结果
# 大模型识别到prompt_template字段指向 weather_report 这个函数模板,
# 会调用这个函数,并传入模板参数
# 大模型自动用 template_args 中的值填充 weather_report 模板中的对应参数。
# 填充后的模板会成为大模型自己的 “思考提示”,然后大模型会根据这个提示,生成最终的回答。
# 这就像我们去餐厅点餐:
# mcp.prompt()的定义就相当于菜单上的标准食谱
# weather_prompt 函数相当于收集食材
# 当模型调用这个weather_report工具时,
# 模型接收到了食谱(prompt_template)和食材(template_args)
# 模型就相当于厨师,按照食谱(提示模板),使用食材(模板参数)烹饪菜品(回答)
return {
"raw_data": weather_result,
"prompt_template": "weather_prompt",
"template_args": {
"city": city_name,
"weather_desc": weather_desc,
"temp": temp_value,
"temp_unit": temp_unit,
"humidity": humidity_value,
"wind_speed": wind_speed,
"speed_unit": speed_unit,
}
}
测试
继续使用cursor,项目重新启动一下。