模型上下文协议MCP
Anthropic(Claude 模型的母公司) 推出了模型上下文协议 MCP,该协议旨在统一大型语言模型(LLM)与外部数据源和工具之间的通信协议。
MCP 主要是为了解决当前 AI 模型因数据孤岛限制,无法充分发挥潜力的难题,MCP 使得 AI 应用能够安全地访问和操作本地及远程数据,为 AI 应用提供了连接万物的接口。
MCP架构

当大模型选择了工具之后,需要有一个工具调用程序,去实际的进行工具的调用,例如通过 Rest API 访问墨迹天气获取天气预报等等。


MCP 把工具调用程序做成了一个 C-S 架构,工具的实际调用由 MCP Server 来完成。


当大模型选择了合适的能力后,MCP Hosts 会调用 MCP Cient 与 MCP Server 进行通信,由 MCP Server 调用工具或者读取资源后,反馈给 MCP Client,然后再由 MCP Hosts 反馈给大模型,由大模型判断是否能解决用户的问题。如果解决了,则会生成自然语言响应,最终由 MCP Hosts 将响应展示给用户。
配置MCP Server
演示一个通过 PostgreSQL MCP Server 使 DeepSeek 能够基于 PostgreSQL 中的数据来回答问题。MCP Server 支持 NodeJS 和 python 两种语言开发,本次案例使用的是 MCP 官方提供的 PostgreSQL MCP Server,其开发语言是 NodeJS ,因此大家需要根据自己的操作系统安装好 NodeJS 。Windows 用户可以点击该链接,下载安装。

创建数据库表
-- 创建数据库
CREATE DATABASE achievement;
-- 连接到新创建的数据库
\c achievement;
-- 创建用户信息 users 表
CREATE TABLE users (
user_id SERIAL PRIMARY KEY,
name VARCHAR(50) NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL
);
-- 创建绩效得分 score 表
CREATE TABLE score (
score_id SERIAL PRIMARY KEY,
score DECIMAL(10, 2) NOT NULL,
user_id INT REFERENCES users(user_id)
);
-- 插入示例数据
INSERT INTO users (name, email) VALUES
('张三', 'zs@example.com'),
('李四', 'ls@example.com'),
('王五', 'ww@example.com');
INSERT INTO score (score, user_id) VALUES
(87.75, 1),
(97.50, 2),
(93.25, 3);
修改MCP配置文件
{
"mcpServers": {
"postgres": {
"command": "node",
"args": [
"D:\\Program Files\\nodejs\\node_modules\\npm\\bin\\npx-cli.js",
"-y",
"@modelcontextprotocol/server-postgres",
"postgresql://postgres:postgres@<你的postgres所在的服务器的IP>:5432/achievement"
]
}
}
}完成配置后,会在左侧看到已连接的 MCP Server,并且会列出支持的 Tools 和 Resources。

首先,我们提问“数据库中有哪些表?

尝试一个难一点的案例,提问“张三,李四,王五的绩效谁高?”在我们的数据库中有 users 和 score 两张表,users 存储了人员姓名和邮箱信息,score 表存储了人员的绩效得分,查询绩效分时需要使用 users 表的主键去查询,因此这是一个两个表联合查询的案例。输出结果如下:

,DeepSeek 一开始直接尝试在 score 表中,用 name 字段去查询绩效,结果发现表中没有 name 字段,因此就开始获取 score 表的表结构。之后又查出了 users 表的数据,最后来了一个两个表的联合查询,得到了正确结果。
实现一个MCP Server
MCP Server 的三大能力

使用python创建MCP Server
pip install uv==0.5.24

使用Tool能力
MCP Server 的 Python SDK,分为 FastMCP SDK 和 Low-Lever SDK 两种。FastMCP 是在 Low-Level 的基础上又做了一层封装,不论是写代码,还是项目依赖等,操作起来都更加简单。
from mcp.server.fastmcp import FastMCP
# Create an MCP server
mcp = FastMCP("achievement")
# Add an get score tool
@mcp.tool()
def get_score_by_name(name: str) -> str:
"""根据员工的姓名获取该员工的绩效得分"""
if name == "张三":
return "name: 张三 绩效评分: 85.9"
elif name == "李四":
return "name: 李四 绩效评分: 92.7"
else:
return "未搜到该员工的绩效"
修改 Roo Code 的 MCP Server 配置文件
{
"mcpServers": {
"achievement": {
"command": "uv",
"args": [
"run",
"--with",
"mcp[cli]",
"--with-editable",
"D:\\workspace\\python\\mcp-test\\achievement",
"mcp",
"run",
"D:\\workspace\\python\\mcp-test\\achievement\\server.py"
]
}
}
}

使用Resource能力
Resource 定义了大模型可以只读访问的数据源,可以用于为大模型提供上下文。




使用Prompt能力
预设了一些 prompt 模板,这样在对话时,可以直接选择模板进行对话,就不用再手敲 prompt 了。
@mcp.prompt()
def prompt(name: str) -> str:
"""创建一个 prompt,用于对员工进行绩效评价"""
return f"""绩效满分是100分,请获取{name}的绩效评分,并给出评价"""


实现一个MCP Client

SSE方式
Server-Sent Events(SSE,服务器发送事件)是一种基于 HTTP 协议的技术,允许服务器向客户端单向、实时地推送数据。在 SSE 模式下,客户端通过创建一个 EventSource 对象与服务器建立持久连接,服务器则通过该连接持续发送数据流,而无需客户端反复发送请求。MCP Python SDK 使用了 Starlette 框架来实现 SSE。
SSE 模式下客户端通过访问 Server 的 /messages 端点发送 JSON-RPC 调用,并通过 /sse 端点获取服务器推送的 JSON-RPC 消息。
实现一个SSE服务器
def create_starlette_app(mcp_server: Server, *, debug: bool = False) -> Starlette:
"""Create a Starlette application that can server the provied mcp server with SSE."""
sse = SseServerTransport("/messages/")
async def handle_sse(request: Request) -> None:
async with sse.connect_sse(
request.scope,
request.receive,
request._send,
) as (read_stream, write_stream):
await mcp_server.run(
read_stream,
write_stream,
mcp_server.create_initialization_options(),
)
return Starlette(
debug=debug,
routes=[
Route("/sse", endpoint=handle_sse),
Mount("/messages/", app=sse.handle_post_message),
],
)

另一个改造点需要创建 MCP 服务器实例,然后通过上面定义的 create_starlette_app 方法创建 Starlette 应用,最后使用 uvicorn 启动 ASGI 服务器,实现实时的 SSE 数据传输。代码如下
if __name__ == "__main__":
mcp_server = mcp._mcp_server
parser = argparse.ArgumentParser(description='Run MCP SSE-based server')
parser.add_argument('--host', default='0.0.0.0', help='Host to bind to')
parser.add_argument('--port', type=int, default=18080, help='Port to listen on')
args = parser.parse_args()
# Bind SSE request handling to MCP server
starlette_app = create_starlette_app(mcp_server, debug=True)
uvicorn.run(starlette_app, host=args.host, port=args.port)
客户端代码改造
客户端的改造会相对简单,就是使用 sse_client 替换 stdio_client,并在初始化时传入 MCP Server 的 HTTP 访问地址。
async def connect_to_sse_server(server_url: str):
"""Connect to an MCP server running with SSE transport"""
# Store the context managers so they stay alive
async with sse_client(url=server_url) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
# List available tools to verify connection
print("Initialized SSE client...")
print("Listing tools...")
response = await session.list_tools()
tools = response.tools
print("\nConnected to server with tools:", [tool.name for tool in tools])
# call a tool
score = await session.call_tool(name="get_score_by_name",arguments={"name": "张三"})
print("score: ", score)

实现求职助手
搜索岗位工具
@mcp.tool(description="根据求职者的期望岗位获取岗位列表数据")
def get_joblist_by_expect_job(job: str) -> str:
"""根据求职者的期望岗位获取岗位列表数据"""
# 取得岗位信息
#with open('job.txt', 'r', encoding='utf-8') as f:
# jobs = f.read()
#以下就是无头浏览器获取岗位的代码
jobs = listjob_by_keyword(job)
return jobs岗位匹配工具
@mcp.tool(description="根据求职者的简历获取适合该求职者的岗位以及求职建议")
def get_job_by_resume(jobs: str, resume: str) -> str:
"""根据求职者的简历获取适合该求职者的岗位以及求职建议"""
#将简历以及岗位列表注入到 prompt 模板
prompt = Job_Search_Prompt.format(resume=resume,job_list=jobs)
messages = [{"role": "user", "content": prompt}]
self.logger.info(f"prompt: {prompt}")
#发送给 ds
response = LLMClient.send_messages(self,messages)
response_text = response.choices[0].message.content
return response_text测试效果
{
"mcpServers": {
"jobsearch": {
"command": "uv",
"args": [
"--directory",
"D:\\workspace\\python\\mcp-test\\jobsearch-mcp-server\\src\\jobsearch_mcp_server",
"run",
"jobsearch-mcp-server"
]
}
}
}请求prompt
以下是我的简历,请帮我匹配合适的工作。
- 姓名:张三
- 专业技能:精通 AI Agent,RAG 开发
- 工作经验:5年
- 教育背景:本科
- 期望薪资:30K
使用AI优化简历内容
AI读取本地简历文件
python 语言中,有一个名叫 python-docx 的包,可以实现读取 docx 文件
uv pip install python-docximport os
from docx import Document
def read_word_file(file_path):
"""读取Word文档内容"""
if not os.path.exists(file_path):
raise FileNotFoundError(f"File not found: {file_path}")
file_path = os.path.abspath(file_path)
file_ext = os.path.splitext(file_path)[1].lower()
if file_ext == '.docx':
return _read_docx(file_path)
else:
raise ValueError(f"Unsupported file format: {file_ext}")
def _read_docx(file_path):
"""读取.docx文件"""
try:
doc = Document(file_path)
full_text = []
# 获取所有段落
for para in doc.paragraphs:
if para.text:
full_text.append(para.text)
# 获取所有表格内容
for table in doc.tables:
for row in table.rows:
row_text = []
for cell in row.cells:
if cell.text:
row_text.append(cell.text)
if row_text:
full_text.append(" | ".join(row_text))
return "\n".join(full_text)
except Exception as e:
raise然后封装成MCP tool
from typing import Any
from ..word.word import read_word_file
class ResumeTools():
def register_tools(self, mcp: Any):
"""Register job tools."""
@mcp.tool(description="读取指定路径的word文件")
def get_word_by_filepath(filepath: str) -> list:
"""根据文件路径获取word文件内容"""
content=read_word_file(filepath)
return content

RAG原理
RAG,中文叫检索增强生成,你可以理解为是开卷考试。我们在考试时,遇到不会的问题,去翻一下书,找到相关内容,然后根据书上内容答题。这个过程的重点是找到相关内容。


此外,当我们使用 RAG 技术时,为了避免文本过长,导致超出大模型的上下文限制,是需要对文本进行切割,然后一段一段地进行向量化,存入到向量数据库当中的。
docker安装向量数据库
docker run -d -name qdrant -p 6333:6333 -v /root/qdrant_data:/qdrant/storage docker.1ms.run/qdrant/qdrant:latest首先将 word 文档加载、切片,然后转成向量塞入向量数据库。之后,当用户提问问题时,先将用户的问题转成向量,在向量数据库中匹配。匹配到合适的片段后,将片段与用户问题一起喂给大模型,由大模型给出最终的回复。
# pip install python-docx
from langchain_community.document_loaders import UnstructuredWordDocumentLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
import nltk
def load_doc():
#nltk.download('punkt_tab')
#nltk.download('averaged_perceptron_tagger')
word=UnstructuredWordDocumentLoader('E:\\AI\\个人简历.docx')
docs=word.load()
splitter = RecursiveCharacterTextSplitter(chunk_size=50,
chunk_overlap=20, )
s_docs=splitter.split_documents(docs)接下来就是将文本转向量以及入向量数据库,依然是使用 LangChain 封装好的工具。这里转向量的大模型,我们使用通义千问的向量模型 text-embedding-v1
def TongyiEmbedding()->DashScopeEmbeddings:
api_key=os.environ.get("dashscope")
return DashScopeEmbeddings(dashscope_api_key=api_key,
model="text-embedding-v1")
def QdrantVecStoreFromDocs(docs:List[Document]):
eb=TongyiEmbedding()
return QdrantVectorStore.from_documents(docs,eb,url="http://<你的公网IP>:6333")
vec_store=QdrantVecStoreFromDocs(s_docs)
用户提问代码如下
def DeepSeek():
return ChatOpenAI(
model="deepseek-chat",
api_key=os.environ.get("deepseek"),
base_url="https://api.deepseek.com"
)
llm=DeepSeek()
prompt = hub.pull("rlm/rag-prompt")
chain = {"context": vec_store.as_retriever() | format_docs,
"question": RunnablePassthrough()} | prompt | llm | StrOutputParser()
ret=chain.invoke("请输出姓名.格式如下\n姓名: ?")
print(ret)
ret = chain.invoke("总结专业技能情况,内容可能包含golang、AI Agent、python、rag等.格式如下\n专业技能: ?")
print(ret)
ret=chain.invoke("根据各大公司工作过的年份总结工作经验有多少年.格式如下\n工作经验: ?年")
print(ret)使用大模型完善简历
完善简历重点在于 prompt 的编写
ResumePrompt = """
你是一个 AI 简历助手。我会给你提供我的简历以及某公司的详细岗位要求。你的任务是根据公司的岗位要求, 帮我改写和完善我的简历,使我的简历符合该公司的要求。
简历:
{resume}
岗位要求:
{input}
"""def load_doc() -> list:
word=UnstructuredWordDocumentLoader('E:\\AI\\个人简历.docx')
docs=word.load()
return docs
def fix_resume():
prompt=PromptTemplate.from_template(ResumePrompt2)
llm=DeepSeekR1()
docs=load_doc()
chain={
"resume": lambda _: docs,
"input":RunnablePassthrough()
} | prompt | llm | StrOutputParser()
ret=chain.invoke(load_jobs())
print(ret)我是将岗位需求抓下来后,放在了 job.txt 中。之后在 fix_resume 方法中,首先导入 prompt 模板,然后初始化 DeepSeek 客户端,之后重点代码就在于构建 chain,chain 其实很好理解,就类似于管道操作。
这个 chain 的意思是首先通过 “input”:RunnablePassthrough(),为 prompt 模板中的 input 字段赋值,这个值是 chain.invoke 赋的。之后把 input 的值传递给了 prompt,然后把 prompt 传递给 llm 大模型,最后把大模型的返回结果传递给 StrOutputParser(),让其将大模型的返回结果解析成字符串输出。
根据模板进一步完善
在提示词中加一个模板,让其在项目经历等关键部分,按照模板进行改写。完善后的提示词如下
ResumePrompt2 = """
你是一个 AI 简历助手。我会给你提供我的简历以及某公司的详细岗位要求。你的任务是根据公司的岗位要求, 帮我改写和完善我的简历,使我的简历符合该公司的要求。
此外,我还会给你一个简历模板,模板中会包含简历中部分内容的大纲,当你匹配到我的简历中有模板提及的内容时,要按照我模板的格式进行编写。
简历:
{resume}
简历模板:
专业技能
请在此描述符合职位要求的技能,尤其是编程技能
项目经验
(1) 项目描述
(2) 我在项目中的角色
(3) 项目规模
(4) 技术堆栈
(5) 已开发模块的描述
(6) 解决难题的经验
岗位要求:
{input}
"""地图服务接入MCP
SSE 是一个可以建立长连接的轻量级协议。使用 SSE 而不使用 HTTP 的原因是,大模型调用 Tool 的过程不是一轮就结束的,往往是需要多次反复调用。因此,设法保持一个长连接,而不是每一次调用 Tool 的时候就得开个新连接,就显得非常重要了。
API接入
from llm import client
import requests
tools = [
{
"name": "get_location_coordinate",
"description": "根据POI名称获取其经纬度坐标",
"parameters": {
"type": "object",
"properties": {
"keywords": {
"type": "string",
"description": "POI名称(中文)",
},
"region": {
"type": "string",
"description": "POI所在的区域名(中文)",
}
},
"required": ["keywords"]
},
},
{
"name": "search_nearby_pois",
"description": "搜索指定坐标附近的POI",
"parameters": {
"type": "object",
"properties": {
"keywords": {
"type": "string",
"description": "目标POI的关键字",
},
"location": {
"type": "string",
"description": "中心点坐标(经度,纬度)",
}
},
"required": ["keywords"]
},
}
]
def http_get(url, params=None):
"""
通用的HTTP GET请求处理函数
Args:
url (str): 完整的请求URL
params (dict, optional): URL参数字典
Returns:
dict: 包含success和data/message的响应结果
"""
try:
response = requests.get(url, params=params)
response.raise_for_status()
return {"success": True, "data": response.json()}
except Exception as e:
return {"success": False, "message": str(e)}
# API配置
AMAP_BASE_URL = "https://restapi.amap.com/v5/place"
AMAP_KEY = "xxxxxxxxxxxxxxxxx"
def get_location_coordinate(keywords, region=None):
"""根据POI名称获取其经纬度坐标"""
params = {
"key": AMAP_KEY,
"keywords": keywords
}
if region:
params["region"] = region
result = http_get(f"{AMAP_BASE_URL}/text", params)
if result["success"] and result["data"].get("pois"):
poi = result["data"]["pois"][0]
return f"POI: {keywords}, 坐标: {poi['location']}"
return "未找到该POI的坐标信息"
def search_nearby_pois(keywords, location=None):
"""搜索指定坐标附近的POI"""
params = {
"key": AMAP_KEY,
"keywords": keywords
}
if location:
params["location"] = location
result = http_get(f"{AMAP_BASE_URL}/around", params)
if result["success"] and result["data"].get("pois"):
results = []
for poi in result["data"]["pois"][:3]:
results.append(f"名称: {poi['name']}, 地址: {poi['address']}, 坐标: {poi['location']}")
return "\n".join(results)
return "未找到符合条件的POI"
















