文章主要讲解了如何使用 MCP(Model Context Protocol)与大型语言模型(LLM)结合来实现工具调用,以及如何搭建一个简单的应用来展示这一过程。
1. 安装一些必要的包
pip install "mcp[cli]"
pip install openai langchain langchain-mcp-adapters langgraph langchain_ollama -U
启动本地模型,这里使用ollama启动个1.5b的小模型
ollama run qwen2.5:1.5b
代码结构:
2. MCP server.py 服务端
check_child_study_situation
:检查小孩最近的学习状况
{
"小明": "学习很努力 from Michael阿明老师点评",
"小红": "学习一般",
"小刚": "学习不太好",
}
-
query_student_scores
:获取学习成绩得分
-
使用
FastMCP
类创建 MCP 服务器,并通过@mcp.tool()
装饰器注册工具函数。
from mcp.server.fastmcp import FastMCP
import os
mcp = FastMCP("Michael's tools")
@mcp.tool()
def check_child_study_situation(name: str) -> str:
"""检查小孩最近的学习状况"""
db = {
"小明": "学习很努力 from Michael阿明老师点评",
"小红": "学习一般",
"小刚": "学习不太好",
}
print(f"Checking study status for {name}")
return db.get(name, "没有找到这个小孩的学习记录")
@mcp.tool()
def query_student_scores(name: str) -> str:
"""查询学生成绩
Args:
name: 学生姓名
Returns:
学生成绩信息,如果未找到则返回相应提示
"""
file_path = os.path.join(os.path.dirname(__file__), "score_points.txt")
try:
with open(file_path, "r", encoding="utf-8") as f:
content = f.read()
# 解析文件内容
students = {}
current_student = None
for line in content.split("\n"):
line = line.strip()
if not line:
continue
if line.endswith(":"): # 学生名
current_student = line[:-1] # 去掉结尾的冒号
students[current_student] = {}
elif current_student and ":" in line: # 科目分数
subject, score = line.split(":")
students[current_student][subject.strip()] = score.strip()
# 返回学生成绩
if name in students:
result = f"{name}的成绩:\n"
for subject, score in students[name].items():
result += f"- {subject}: {score}\n"
return result
else:
return f"没有找到{name}的成绩记录"
except Exception as e:
return f"查询成绩出错: {str(e)}"
@mcp.tool()
def add(a: int, b: int) -> int:
"""Add two numbers"""
return a + b
@mcp.tool()
def multiply(a: int, b: int) -> int:
"""Multiply two numbers"""
return a * b
if __name__ == "__main__":
mcp.run(transport="stdio")
3. fastapi 启动 api 服务
- 搭建了一个简单的 Web 服务来与 MCP 服务器交互。
- 通过加载 MCP 工具并创建反应式代理(react agent)来处理用户请求。
- 使用 Server-Sent Events (SSE) 实现流式响应,模拟实时对话效果。
from fastapi import FastAPI, Request
from fastapi.responses import StreamingResponse, HTMLResponse, FileResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from sse_starlette.sse import EventSourceResponse
from pathlib import Path
import asyncio
import json
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
from langchain_mcp_adapters.tools import load_mcp_tools
from langgraph.prebuilt import create_react_agent
from langchain_ollama import ChatOllama
# Get the current directory
BASE_DIR = Path(__file__).resolve().parent
# Initialize FastAPI
app = FastAPI(title="MCP Chat with SSE")
# Mount static files directory
app.mount("/static", StaticFiles(directory=str(BASE_DIR / "static")), name="static")
# Initialize the model
model = ChatOllama(model='qwen2.5:1.5b')
# Configure the MCP server parameters
server_params = StdioServerParameters(
command="python",
args=["server.py"],
)
# Create a directory for static files if it doesn't exist
static_dir = BASE_DIR / "static"
static_dir.mkdir(exist_ok=True)
# Add endpoint to serve the chat interface
@app.get("/", response_class=FileResponse)
async def get_chat_interface():
"""Serve the chat interface HTML page"""
return FileResponse(path=str(BASE_DIR / "static" / "chat.html"))
async def generate_stream(prompt: str):
"""Generate stream of responses from the MCP agent."""
try:
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
tools = await load_mcp_tools(session)
agent = create_react_agent(model, tools)
# 获取代理的响应结果(这是一个协程,需要await)
result = await agent.ainvoke({
"messages": [
{"role": "user", "content": prompt},
]
})
# Debug: Print the structure of the result to understand it
print(f"Result type: {type(result)}")
print(f"Result keys: {result.keys() if isinstance(result, dict) else 'Not a dict'}")
# 提取并返回结果
if isinstance(result, dict) and "messages" in result:
for message in result["messages"]:
# Try different ways to identify assistant messages
is_assistant = False
content = None
# Try common attribute patterns in LangChain messages
if hasattr(message, "role") and message.role == "assistant":
is_assistant = True
content = message.content if hasattr(message, "content") else None
elif hasattr(message, "type") and message.type == "assistant":
is_assistant = True
content = message.content if hasattr(message, "content") else None
# For AIMessage and similar classes
if hasattr(message, "__class__") and "AI" in message.__class__.__name__:
is_assistant = True
content = message.content if hasattr(message, "content") else None
if is_assistant and content:
# 逐字符输出,模拟流式响应
buffer = ""
for char in content:
buffer += char
yield buffer
# 添加一点延迟,使流式效果更明显
await asyncio.sleep(0.01)
return # Stop after finding the first assistant message
# If we didn't find any assistant messages
yield "没有找到助手的回复,请重试。"
except Exception as e:
print(f"Error in generate_stream: {str(e)}")
yield f"Error: {str(e)}"
@app.get("/chat")
async def chat(request: Request, prompt: str):
"""
Endpoint that returns an SSE stream of chat responses.
Query Parameters:
- prompt: The text prompt to send to the model
"""
return EventSourceResponse(
generate_stream(prompt),
media_type="text/event-stream"
)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
4. 对话测试
只需要启动 fastapi服务,然后再web端调用
可以看到,模型有一定的概率能找到相应的 tool 进行调用
5. 代码调用
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
from langchain_mcp_adapters.tools import load_mcp_tools
from langgraph.prebuilt import create_react_agent
from langchain_ollama import ChatOllama
import asyncio
model = ChatOllama(model='qwen2.5:1.5b')
server_params = StdioServerParameters(
command="python",
args=["server.py"],
)
async def run_agent():
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
tools = await load_mcp_tools(session)
agent = create_react_agent(model, tools)
agent_response = await agent.ainvoke({
"messages": [
{"role": "user", "content": "小明最近的学习状态怎么样?"},
]
})
return agent_response
if __name__ == "__main__":
result = asyncio.run(run_agent())
print(result)
输出:
{'messages': [HumanMessage(content='请查询小明得分成绩', additional_kwargs={}, response_metadata={}, id='d24dabad-0d48-4d8e-a6ec-ce4c2df53774'), AIMessage(content='', additional_kwargs={},
response_metadata={'model': 'qwen2.5:1.5b', 'created_at': '2025-04-19T15:39:42.8503337Z', 'done': True, 'done_reason': 'stop', 'total_duration': 1429941000, 'load_duration': 23185900, 'prompt_eval_count': 355, 'prompt_eval_duration': 5802200, 'eval_count': 22, 'eval_duration': 1397424200, 'model_name': 'qwen2.5:1.5b'},
id='run-a36349f2-a6d6-4865-901f-08a57a2b5e12-0',
tool_calls=[{'name': 'query_student_scores', 'args': {'name': '小明'},
'id': '9762c2f2-87b2-48ef-90d6-22cacf837a30', 'type': 'tool_call'}],
usage_metadata={'input_tokens': 355, 'output_tokens': 22, 'total_tokens': 377}),
ToolMessage(content='小明的成绩:\n- 语文: 80\n- 数学: 95\n', name='query_student_scores', id='437e7cd4-4e28-4c40-b7b6-bd2598f2ab12', tool_call_id='9762c2f2-87b2-48ef-90d6-22cacf837a30'),
AIMessage(content='根据查询结果,小明的得分如下:\n\n- 语文成绩:80\n- 数学成绩:95', additional_kwargs={}, response_metadata={'model': 'qwen2.5:1.5b', 'created_at': '2025-04-19T15:39:44.6487902Z', 'done': True, 'done_reason': 'stop', 'total_duration': 1780121800, 'load_duration': 24304300, 'prompt_eval_count': 413, 'prompt_eval_duration': 10838200, 'eval_count': 26, 'eval_duration': 1736960100, 'model_name': 'qwen2.5:1.5b'}, id='run-c03655dc-e0ad-46bf-ac21-effbc4b6b94d-0', usage_metadata={'input_tokens': 413, 'output_tokens': 26, 'total_tokens': 439})]}
6. 总结
在 MCP(Model Context Protocol)中,tool
和 function calling
是紧密相关但又有所区别的概念,它们在实现大型语言模型(LLM)与外部功能交互时,具有各自特定的作用和意义:
-
MCP 中的 tool
- 是定义在 MCP 服务器上的功能模块,通过 MCP 协议暴露给客户端。
- 具有封装性、可发现性和异步性等特点。
-
Function calling
- 是 LLM 调用外部函数的能力,增强了模型的能力,使其能够借助外部资源解决问题。
- 特点包括增强模型能力、动态交互、参数传递与结果处理等。
-
二者关系
- MCP 中的 tool 是具体的工具实现,function calling 是调用这些工具的方式。
- MCP 是协议规范,定义了 LLM 和工具之间的通信方式;function calling 是 LLM 的能力,利用 MCP 协议调用工具。
- 二者紧密协作,共同实现复杂的应用场景。
本文 from https://michael.blog.csdn.net/