前言
之前的示例用的都是MCP的官方SDK(版本 1.14.0),简单使用还是没问题的,但对于Sampling、Elicitation这些相对高级的功能,官方没有提供Demo,而且因为比较新,网上也没搜到能用的案例。以我自己的水平折腾了一天也没捣鼓出来。在翻mcp源码时意外发现了其内置的FastMCP,顺藤摸瓜找到了FastMCP的官网,在官方文档中找到了相关用法。这里我们就用FastMCP来实现之前用mcp官方sdk做的功能,看看它有什么优势。
安装
截至本文日期的fastmcp版本为 2.12.2- # uv
- uv add fastmcp
- # pip
- python -m pip install fastmcp
复制代码 MCP Server
MCP Server的写法跟之前使用mcp官方sdk差不多,只是导入FastMCP的地方和运行配置不太一样。- from fastmcp import FastMCP
- from typing import TypeAlias, Union
- from datetime import datetime
- import asyncio
- import asyncssh
- mcp = FastMCP("custom")
- Number: TypeAlias = Union[int, float]
- @mcp.tool()
- def add(a: Number, b: Number) -> Number:
- """Add two numbers"""
- return a + b
- @mcp.tool()
- def multiply(a: Number, b: Number) -> Number:
- """Multiply two numbers"""
- return a * b
- @mcp.tool()
- def is_greater_than(a: Number, b: Number) -> bool:
- """Check if a is greater than b
-
- Args:
- a (Number): The first number
- b (Number): The second number
- Returns:
- bool: True if a is greater than b, False otherwise
- """
- return a > b
- @mcp.tool()
- async def get_weather(city: str) -> str:
- """Get weather for a given city."""
- return f"It's always sunny in {city}!"
- @mcp.tool()
- async def get_date() -> str:
- """Get today's date."""
- return datetime.now().strftime("%Y-%m-%d")
- @mcp.tool()
- async def execute_ssh_command_remote(hostname: str, command: str) -> str:
- """Execute an SSH command on a remote host.
-
- Args:
- hostname (str): The hostname of the remote host.
- command (str): The SSH command to execute.
- Returns:
- str: The output of the SSH command.
- """
- try:
- async with asyncssh.connect(hostname, username="rainux", connect_timeout=10) as conn:
- result = await conn.run(command, timeout=10)
- stdout = result.stdout
- stderr = result.stderr
- content = str(stdout if stdout else stderr)
- return content
- except Exception as e:
- return f"Error executing command '{command}' on host '{hostname}': {str(e)}"
-
- @mcp.tool()
- async def execute_command_local(command: str, timeout: int = 10) -> str:
- """Execute a shell command locally.
-
- Args:
- command (str): The shell command to execute.
- timeout (int): Timeout in seconds for command execution. default is 10 seconds.
- Returns:
- str: The output of the shell command.
- """
- try:
- proc = await asyncio.create_subprocess_shell(
- command,
- stdout=asyncio.subprocess.PIPE,
- stderr=asyncio.subprocess.PIPE
- )
- stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout)
- stdout_str = stdout.decode().strip()
- stderr_str = stderr.decode().strip()
- # content = stdout.decode() if stdout else stderr.decode()
- if stdout_str:
- return f"Stdout: {stdout_str}"
- elif stderr_str:
- return f"Stderr: {stderr_str}"
- else:
- return "Command executed successfully with no output"
- except asyncio.TimeoutError:
- if proc and not proc.returncode:
- try:
- proc.terminate()
- await proc.wait()
- except:
- pass
- return f"Error: Command '{command}' timed out after {timeout} seconds"
- except Exception as e:
- return f"Error executing command '{command}': {str(e)}"
- if __name__ == "__main__":
- mcp.run(transport="http", host="localhost", port=8001, show_banner=False)
复制代码 因为使用http协议,所以运行client前要先运行server,顺带测试下能否正常启动。
MCP Client
FastMCP的client写法与mcp官方sdk用法大致上也差不多,但在一些细节上更加友好。- """
- MCP客户端示例程序
- 该程序演示了如何使用MCP协议与服务器进行交互,并通过LLM处理用户查询。
- """
- import asyncio
- import json
- import readline # For enhanced input editing
- import traceback
- from typing import cast
- from openai.types.chat import ChatCompletionMessageFunctionToolCall
- from fastmcp import Client
- from openai import AsyncOpenAI
- from pkg.config import cfg
- from pkg.log import logger
- class MCPHost:
- """MCP主机类,用于管理与MCP服务器的连接和交互"""
-
- def __init__(self, server_uri: str):
- """
- 初始化MCP客户端
-
- Args:
- server_uri (str): MCP服务器的URI地址
- """
- # 初始化MCP客户端连接
- self.mcp_client: Client = Client(server_uri)
- # 初始化异步OpenAI客户端用于与LLM交互
- self.llm = AsyncOpenAI(
- base_url=cfg.llm_base_url,
- api_key=cfg.llm_api_key,
- )
- # 存储对话历史消息
- self.messages = []
- async def close(self):
- """关闭MCP客户端连接"""
- if self.mcp_client:
- await self.mcp_client.close()
- async def process_query(self, query: str) -> str:
- """Process a user query by interacting with the MCP server and LLM.
-
- Args:
- query (str): The user query to process.
- Returns:
- str: The response from the MCP server.
- """
- # 将用户查询添加到消息历史中
- self.messages.append({
- "role": "user",
- "content": query,
- })
- # 使用异步上下文管理器确保MCP客户端连接正确建立和关闭
- async with self.mcp_client:
- # 从MCP服务器获取可用工具列表
- tools = await self.mcp_client.list_tools()
- # 构造LLM可以理解的工具格式
- available_tools = []
- # 将MCP工具转换为OpenAI格式
- for tool in tools:
- available_tools.append({
- "type": "function",
- "function": {
- "name": tool.name,
- "description": tool.description,
- "parameters": tool.inputSchema,
- }
- })
- logger.info(f"Available tools: {[tool['function']['name'] for tool in available_tools]}")
- # 调用LLM,传入对话历史和可用工具
- resp = await self.llm.chat.completions.create(
- model=cfg.llm_model,
- messages=self.messages,
- tools=available_tools,
- temperature=0.3,
- )
- # 存储最终响应文本
- final_text = []
- # 获取LLM的首个响应消息
- message = resp.choices[0].message
- # 如果响应包含直接内容,则添加到结果中
- if hasattr(message, "content") and message.content:
- final_text.append(message.content)
- # 循环处理工具调用,直到没有更多工具调用为止
- while message.tool_calls:
- # 遍历所有工具调用
- for tool_call in message.tool_calls:
- # 确保工具调用有函数信息
- if not hasattr(tool_call, "function"):
- continue
- # 类型转换以获取函数调用详情
- function_call = cast(ChatCompletionMessageFunctionToolCall, tool_call)
- function = function_call.function
- tool_name = function.name
- # 解析函数参数
- tool_args = json.loads(function.arguments)
- # 检查MCP客户端是否已连接
- if not self.mcp_client.is_connected():
- raise RuntimeError("Session not initialized. Cannot call tool.")
-
- # 调用MCP服务器上的指定工具
- result = await self.mcp_client.call_tool(tool_name, tool_args)
- # 将助手的工具调用添加到消息历史中
- self.messages.append({
- "role": "assistant",
- "tool_calls": [
- {
- "id": tool_call.id,
- "type": "function",
- "function": {
- "name": function.name,
- "arguments": function.arguments
- }
- }
- ]
- })
- # 将工具调用结果添加到消息历史中
- self.messages.append({
- "role": "tool",
- "tool_call_id":tool_call.id,
- "content": str(result.content) if result.content else ""
- })
-
- # 基于工具调用结果再次调用LLM
- final_resp = await self.llm.chat.completions.create(
- model=cfg.llm_model,
- messages=self.messages,
- tools=available_tools,
- temperature=0.3,
- )
- # 更新消息为最新的LLM响应
- message = final_resp.choices[0].message
- # 如果响应包含内容,则添加到最终结果中
- if message.content:
- final_text.append(message.content)
-
- # 返回连接后的完整响应
- return "\n".join(final_text)
- async def chat_loop(self):
- """主聊天循环,处理用户输入并显示响应"""
- print("Welcome to the MCP chat! Type 'quit' to exit.")
- # 持续处理用户输入直到用户退出
- while True:
- try:
- # 获取用户输入
- query = input("You: ").strip()
- # 检查退出命令
- if query.lower() == "quit":
- print("Exiting chat. Goodbye!")
- break
- # 跳过空输入
- if not query:
- continue
- # 处理用户查询并获取响应
- resp = await self.process_query(query)
- print(f"Assistant: {resp}")
-
- # 捕获并记录聊天循环中的任何异常
- except Exception as e:
- logger.error(f"Error in chat loop: {str(e)}")
- logger.error(traceback.format_exc())
- async def main():
- """主函数,程序入口点"""
- # 创建MCP主机实例
- client = MCPHost(server_uri="http://localhost:8001/mcp")
- try:
- # 启动聊天循环
- await client.chat_loop()
- except Exception as e:
- # 记录主程序中的任何异常
- logger.error(f"Error in main: {str(e)}")
- logger.error(traceback.format_exc())
- finally:
- # 确保客户端连接被正确关闭
- await client.close()
-
- if __name__ == "__main__":
- # 运行主程序
- asyncio.run(main())
复制代码 FastMCP的客户端API设计更加直观,特别是在连接管理和工具调用方面,代码更简洁易懂。
client运行输出:- Welcome to the MCP chat! Type 'quit' to exit.
- You: 今天的日期是什么
- Assistant: 今天的日期是2025年9月13日。
- You: 检查下 tx 服务器和本地的内存占用情况
- Assistant: 以下是 tx 服务器和本地的内存占用情况:
- ### tx 服务器
- total used free shared buff/cache available
- Mem: 3.7Gi 2.2Gi 207Mi 142Mi 1.7Gi 1.5Gi
- Swap: 0B 0B 0B
- ### 本地
- total used free shared buff/cache available
- Mem: 62Gi 14Gi 38Gi 487Mi 10Gi 48Gi
- Swap: 3.8Gi 0B 3.8Gi
- 从这些信息中可以看出,tx 服务器的内存使用较高,而本地系统仍有较多可用内存。如果需要进一步分析或采取措施,请告诉我!
- You: 再查下硬盘
- Assistant: 已检查 tx 服务器和本地的内存及硬盘占用情况。以下是总结:
- ### tx 服务器
- - **内存占用**:
- - 总内存: 3.7 Gi
- - 已用内存: 2.2 Gi
- - 可用内存: 1.5 Gi
- - **硬盘使用**:
- - 根目录 `/`: 总大小 69G,已用 17G,可用 53G,使用率 24%
- ### 本地
- - **内存占用**:
- - 总内存: 62 Gi
- - 已用内存: 14 Gi
- - 可用内存: 48 Gi
- - **硬盘使用**:
- - 根目录 `/`: 总大小 234G,已用 30G,使用率 14%
- - `/home`: 总大小 676G,已用 197G,使用率 31%
- 如果需要进一步操作,请告知!
- You: quit
- Exiting chat. Goodbye!
复制代码 可以看到,FastMCP的基本运行逻辑是正常的,跟使用MCP官方SDK相差不大,而且还更简洁一点。
小结
使用FastMCP与使用mcp官方sdk相比,整体体验更加友好。FastMCP不仅保持了与官方SDK的兼容性,还在API设计上做了优化,使得代码更加简洁易懂。后续博客中我们会继续使用FastMCP来介绍Sampling、Elicitation等MCP的高级功能。
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |