from fastmcp import FastMCP
mcp = FastMCP("My MCP Server")
@mcp.tool()
def multiply(a: int, b: int) -> int:
"""
返回两个数的乘积
Args:
a: 第一个数
b: 第二个数
Returns:
int: 两个数的乘积
"""
return a * b
@mcp.tool()
def add(a: int, b: int) -> int:
"""
返回两个数的和
Args:
a: 第一个数
b: 第二个数
Returns:
int: 两个数的和
"""
return a + b
@mcp.tool()
def devide(a: int, b: int) -> float:
"""
返回两个数的商
Args:
a: 第一个数
b: 第二个数
Returns:
float: 两个数的商
"""
return a / b
if __name__ == "__main__":
# 3. 启动本地MCP服务器, 传输协议Stdio
# mcp.run()
# 远程访问的http
mcp.run(transport="http", host="0.0.0.0", port=8000)
# 服务器端的同步函数是FastMCP用线程池在异步框架里兜住的
# 而你拿到的工具对象之所以只有异步接口,是langchain_mcp_adapters单方面包装的结果
# 和你的FastMCP代码怎么写无关
import os
from langchain.tools import tool
from langchain.chat_models import init_chat_model
from langchain_mcp_adapters.client import MultiServerMCPClient
import dotenv
dotenv.load_dotenv()
# 定义模型和工具
model = init_chat_model(
model="deepseek-chat",
temperature=0.0
)
# @tool
# def multiply(a: int, b: int) -> int:
# """
# 返回两个数的乘积
# Args:
# a: 第一个数
# b: 第二个数
# Returns:
# int: 两个数的乘积
# """
# return a * b
# @tool
# def add(a: int, b: int) -> int:
# """
# 返回两个数的和
# Args:
# a: 第一个数
# b: 第二个数
# Returns:
# int: 两个数的和
# """
# return a + b
# @tool
# def devide(a: int, b: int) -> float:
# """
# 返回两个数的商
# Args:
# a: 第一个数
# b: 第二个数
# Returns:
# float: 两个数的商
# """
# return a / b
# tools = [multiply, add, devide]
# 从MCP获取服务工具
async def get_mcp_tools():
client = MultiServerMCPClient(
{
"my_math_server": {
"transport": "streamable_http",
"url": "http://localhost:8000/mcp"
}
}
)
# 获取工具列表,这些 工具已经是 LangChain 兼容的格式
tools = await client.get_tools()
return tools
# 定义状态
import operator
from langchain.messages import AnyMessage, AIMessage
from typing_extensions import TypedDict, Annotated
# 定义模型节点
from langchain.messages import SystemMessage, HumanMessage
class MessagesState(TypedDict):
# 首次写入(作为初始输入时),它是直接赋值的
messages: Annotated[list[AnyMessage], operator.add]
llm_calls: int
async def main():
tools = await get_mcp_tools()
print("==============tools==============")
print(tools)
print("==============tools==============")
tools_by_name = {tool.name: tool for tool in tools}
# 把 MCP 工具绑定到模型
models_with_tools = model.bind_tools(tools)
def llm_call(state: MessagesState):
"""
models_with_tools.invoke(
{
"messages": [AIMessage(...)], # 只含一条新消息的列表
"llm_calls": state["llm_calls"] + 1
}
operator.add对两个list做拼接,所以最终messages = 历史消息 + [新的AIMessage消息]
"""
return {
"messages": [
# 返回值时单条AIMessage消息
# 返回结构是:
models_with_tools.invoke(
[
SystemMessage(
content="你是一个乐于助人的助手,负责对一组输入进行算数运算"
)
]
+ state["messages"]
)
],
"llm_calls": state["llm_calls"] + 1
}
# 定义工具节点:工具节点用于调用工具并返回结果
from langchain.messages import ToolMessage
async def tool_node(state: MessagesState):
"""执行工具调用"""
result = []
for tool_call in state["messages"][-1].tool_calls:
tool = tools_by_name[tool_call["name"]]
observation = await tool.ainvoke(tool_call["args"])
result.append(
ToolMessage(content=str(observation), tool_call_id=tool_call["id"])
)
return {"messages": result}
# 定义 end 逻辑: 条件边函数用于根据 LLM 是否进行了工具调用来路由到工具节点或终端
from typing import Literal
from langgraph.graph import StateGraph, START, END
# 路由函数
def should_continue(state: MessagesState) -> Literal["tool_node", "end"]:
messages = state["messages"]
last_message = messages[-1]
print("==============last_message==============")
print(last_message)
print("==============last_message==============")
if last_message.tool_calls:
return "tool_node"
return "end"
# 构建和编译Agent
builder = StateGraph(MessagesState)
builder.add_node("llm_call", llm_call)
builder.add_node("tool_node", tool_node)
builder.add_edge(START, "llm_call")
builder.add_conditional_edges(
"llm_call",
should_continue,
{"tool_node": "tool_node", "end": END}
)
builder.add_edge("tool_node", "llm_call")
graph = builder.compile()
with open("graph.png", "wb") as f:
f.write(graph.get_graph().draw_mermaid_png())
messages = [HumanMessage(content="10 * 20 = ?,结果已josn格式返回,一个question,一个answer。")]
res = await graph.ainvoke({"messages": messages, "llm_calls": 0})
print(res)
print("==============last messages==============")
print(res["messages"][-1])
print(res["messages"][-1].content)
if __name__ == "__main__":
import asyncio
# 建立事件循环, 把main这个协程放进去
# 从此以后,所有await的东西都在这个循环里调度,这是异步启动的含义
asyncio.run(main())
# LangGraph对节点函数的签名没有统一强制要求异步,一个图里可以混合同步节点和异步节点
# graph.ainvoke会妥善处理,遇到同步节点就放到线程池里去执行,遇到异步节点就在事件循环里执行
# 事件循环是主干,同步函数不会自动变成异步,而是被langchain/langgraph用run_in_executor包装成可await的任务
# 丢进线程池执行,执行完在回到事件循环继续
# 为什么异步里面要有线程池?
# 因为同步函数里如果直接做阻塞IO(比如:request.get、time.sleep),会卡死整个事件循环,导致所有其他协程都无法推进
# 把他们扔进线程池,事件循环本身不被堵住。这是asyncio生态里同步代码兼容异步框架的标准做法
# 一个容易混淆的点:
# 线程池不是给所有同步函数准备的默认通道。只有被异步框架显示调度的同步函数才会进线程池。
# 你自己在main()里调用一个同步函数foo(),它就是在当前线程(事件循环所在线程)里同步执行的,不会自动进线程池
# 是LangGraph这类框架在调用节点时主动用了 run_in_executor 包装,才进了线程池
# 不是异步走循环、同步走线程池的静态二分,而是循环为主干,同步函数按需被框架调度到线程池的动态过程
# 事件循环是单线程的,它靠某个协程遇到await时让出控制权,去跑别的协程来实现并发,
# 同步函数里如果没有await,(time.sleep、request.get、同步数据库查询都是),它就一口气占着线程跑完才返回
# 循环没有任何机会切出去,并发的假象直接破灭,所有其他协程一起被卡住
# 我用了 asyncio.run 启动,所以整段代码都是异步的,这个理解不对,asyncio.run 只是创建了一个事件循环并运行main协程
# 它不会把里面所有函数都变成非阻塞的,是不是非阻塞,取决于每个函数自己有没有让出控制权,一个协程里塞满同步调用
# 它在循环里跑起来和普通同步代码一样卡,异步框架提供的是"让出控制权"的机制,不是自动把同步代码变块
# 一句话:LangGraph 替你做了线程池调度,自己写就没有这层保护,同步函数会实打实阻塞事件循环,必须手动 awaig asyncio.to_thread或者 loop.run_in_executor包装