简介
构建MCP服务端的目的是用于串通我的项目中各种结构和零件 MCP可以分离代码的llm的工作部分,大模型专注于它最擅长的"思考",而我们的代码专注于我们擅长的"执行"。这使得整个系统更加清晰和模块化。 为此,我将讲一下构建MCP的流程
MCP服务端的构建
然后对于mcp函数的内部 首先导入必要的包
python
import os # 获取地址
import logging # 导出日志,可选
from mcp.server.fastmcp import FastMCP # 导入MCP服务器框架
初始化服务器
ini
mcp = FastMCP("file-reader")
定义一个工具,这个工具就是llm会调用的工具列表,是整个服务器最重要的部分 @mcp.tool() 是一个"装饰器" 它告诉 FastMCP 框架:下面这个普通的 Python 函数是一个工具 请把它注册并暴露给外部。、 这里的函数内部可以是任何想拥有的内容,这里只展示读取其他文件的效果
python
@mcp.tool()
async def read_file(filename: str) -> str:
"""
简要分析
Args:(示例)
filename:参数含义
username: 参数含义。
Returns:
一个包含分析结果的JSON字符串。(返回示例。自行修改)
"""
后面必须跟一段函数解释 这一段的解释是为了之后返回给大语言模型的描述 这是对这个函数功能的自然语言描述。这是整个结构中对大模型来说最重要的一部分! 大模型就是通过阅读这段描述来理解"这个工具是干什么用的?"、"我应该在什么时候用它?"。一个清晰、准确的描述,是让大模型能正确使用工具的关键 可以在下列自行添加分析后的效果: 例如 最后写一段MCP持续被监听的函数,这时候MCP就会通过 通过标准输入/输出(stdio)来监听来自客户端的连接和指令。
python
if __name__ == "__main__":
logging.info("--- MCP 文件读取服务器启动 ---")
mcp.run(transport='stdio')
主代码群的初始化MCP
首先需要启动mcp服务,这里是构建两个关键的参数,一个是session,一个是tool 这里由于是直接启动以py文件构建的服务端,所以我们首先要获取该文件的相对路径,然后配置python解释器
python
mcp_server_path = os.path.join(current_dir, "CausalChatMCP", "mcp_server.py")
server_params = StdioServerParameters(command=sys.executable, args=[mcp_server_path])
获取完之后,我们就需要我们就要运行该程序,并建立起主程序和子程序之间的通信 stdio_client(server_params) 启动 mcp_server.py 子进程, 由于mcp需要输入输出流,所以在这里定义两个参数 注意,这里的enter_async_context指的是使用异步上下文管理器管理子进程生命周期 当退出上下文时自动终止子进程,这里的mcp_process_stack一般是异步上下栈的实例
python
read_stream, write_stream = await mcp_process_stack.enter_async_context(stdio_client(server_params))
建立完通信之后,并没有载体进行互连,所以建立一个session进行手法MCP的消息
python
session = await exit_stack.enter_async_context(ClientSession(read_stream, write_stream))
await session.initialize()
建立完通信之后,就需要获取我们在MCP中定义的工具,工具有哪些呢 获取完工具之后,我们需要转换为LLm所需要的格式信息 其中的MCPtool是严格定义的
- "type": "function": 含义: 这是在告诉大模型,我提供给你的这个工具是一个函数(function),你可以像调用函数一样调用它。 是否可变: 不可变。这是API定义好的类型。
- "function": 含义: 这是一个JSON对象,用来包裹这个函数的所有具体信息。 是否可变: 不可变。这是固定的结构。
- "name": tool.name: 含义: 这是函数的名字。这个名字非常重要,它就是这个工具的唯一ID。当大模型决定要调用这个工具时,它就会通过这个名字来告诉我们。 tool.name 的值来自于哪里?来自于您在 py 中定义的函数名
- "description": tool.description: 含义: 这是对这个函数功能的自然语言描述。这是整个结构中对大模型来说最重要的一部分! 大模型就是通过阅读这段描述来理解"这个工具是干什么用的?"、"我应该在什么时候用它?"。一个清晰、准确的描述,是让大模型能正确使用工具的关键。 tool.description 的值来自于哪里?来自于在 py 中为函数编写的文档字符串。
- "parameters": tool.inputSchema: 含义: 这里定义了函数需要哪些参数,以及这些参数的类型和格式。它使用的是一种叫做"JSON Schema"的规范。这等于告诉大模型:"如果你要调用read_file,你必须提供一个名为filename的参数,并且它必须是一个字符串。" tool.inputSchema 的值来自于哪里?来自于 mcp 框架对 参数类型注解的自动解析,例如 filename: str。
那么对于mcp自动回应的参数就是如下格式
python
{
"type": "function",
"function": {
"name": "xx",
"description": "xx",
"inputScheme": {
"username": {
"type": "string"
},
"filename": {
"type": "string"
}
},
"required": ["username"]
}
}
我们获取的mcp_tool结构就如下:
python
tools_response = await session.list_tools()
mcp_tools = [{
"type": "function",
"function": {
"name": tool.name,
"description": tool.description,
"parameters": tool.inputSchema,
}
} for tool in tools_response.tools]
该部分的完整代码如下
python
current_dir = os.path.dirname(os.path.abspath(__file__))
mcp_server_path = os.path.join(current_dir, "CausalChatMCP", "mcp_server.py")
server_params = StdioServerParameters(command=sys.executable, args=[mcp_server_path])
read_stream, write_stream = await mcp_process_stack.enter_async_context(stdio_client(server_params))
session = await mcp_process_stack.enter_async_context(ClientSession(read_stream, write_stream))
await session.initialize()
tools_response = await session.list_tools()
mcp_tools = [{
"type": "function",
"function": {
"name": tool.name,
"description": tool.description,
"parameters": tool.inputSchema,
}
} for tool in tools_response.tools]
mcp_session = session
logging.info(f"MCP服务器连接成功,会话已激活。发现工具: {[tool['function']['name'] for tool in mcp_tools]}")
主代码群的调用MCP
当我们在主程序初始化好了MCP所需要的输入输出流,通信工具之后,我们就需要让LLm调用工具了 那么在openai的标准化定义之中,我们将MCPtool传入response之中,又LLM来决定是否调用MCP
python
response = client.chat.completions.create(
model=current_model,
messages=messages,
tools=mcp_tools or None,
tool_choice="auto",
)
response_message = response.choices[0].message
tool_calls = response_message.tool_calls
这里的tool_calls是市面上的大模型所定义的,所以可以返回我调用了什么工具,或者没有调用 这里返回的结果需要注意 这里的response_message并没有返回一个包含工具的结果,他只返回了一次调用工具的指令 比如
- LLM 理解了用户的意图。
- 它查看了工具清单,发现 read_file 工具正好能满足这个意图。
- 它决定:"好的,我需要调用 read_file,然后就返回给用户了
所以获取到指令之后,需要在主程序中调用他,首先获取工具所调用的名称和参数 首先获取函数名和函数参数,返回给MCP的name和args 用function_response_obj获取返回给MCP返回的响应列表
python
{
"status": "success",
"content": [ // 内容可以是一个列表,允许多种类型的返回
{
"type": "text",
"text": "。。。"
}
],
"metadata": { "execution_time": "0.01s" }
}
通过function_response_tex来获取我们要的东西
python
function_name = tool_call.function.name
function_args = json.loads(tool_call.function.arguments)
function_response_obj = await mcp_session.call_tool(function_name, function_args)
function_response_text = function_response_obj.content[0].text
调用完我们所需要的东西之后,我们就可以第二次输入给llm总结,加入到历史消息之中
python
messages.append({
"tool_call_id": tool_call.id, # 这里id需要对应第一次调用的id
"role": "tool",
"name": function_name,
"content": function_response_text,
})
second_response = client.chat.completions.create(
model=current_model,
messages=messages,
)
summary_text = second_response.choices[0].message.content
这里输出的就是llm所调用的东西了,也就是整个流程需要调用两次。