找回密码
 立即注册
首页 业界区 业界 [MCP][03]使用FastMCP开发MCP应用

[MCP][03]使用FastMCP开发MCP应用

布相 2025-10-1 17:20:21
前言

之前的示例用的都是MCP的官方SDK(版本 1.14.0),简单使用还是没问题的,但对于Sampling、Elicitation这些相对高级的功能,官方没有提供Demo,而且因为比较新,网上也没搜到能用的案例。以我自己的水平折腾了一天也没捣鼓出来。在翻mcp源码时意外发现了其内置的FastMCP,顺藤摸瓜找到了FastMCP的官网,在官方文档中找到了相关用法。这里我们就用FastMCP来实现之前用mcp官方sdk做的功能,看看它有什么优势。
安装

截至本文日期的fastmcp版本为 2.12.2
  1. # uv
  2. uv add fastmcp
  3. # pip
  4. python -m pip install fastmcp
复制代码
MCP Server

MCP Server的写法跟之前使用mcp官方sdk差不多,只是导入FastMCP的地方和运行配置不太一样。
  1. from fastmcp import FastMCP
  2. from typing import TypeAlias, Union
  3. from datetime import datetime
  4. import asyncio
  5. import asyncssh
  6. mcp = FastMCP("custom")
  7. Number: TypeAlias = Union[int, float]
  8. @mcp.tool()
  9. def add(a: Number, b: Number) -> Number:
  10.     """Add two numbers"""
  11.     return a + b
  12. @mcp.tool()
  13. def multiply(a: Number, b: Number) -> Number:
  14.     """Multiply two numbers"""
  15.     return a * b
  16. @mcp.tool()
  17. def is_greater_than(a: Number, b: Number) -> bool:
  18.     """Check if a is greater than b
  19.    
  20.     Args:
  21.         a (Number): The first number
  22.         b (Number): The second number
  23.     Returns:
  24.         bool: True if a is greater than b, False otherwise
  25.     """
  26.     return a > b
  27. @mcp.tool()
  28. async def get_weather(city: str) -> str:  
  29.     """Get weather for a given city."""
  30.     return f"It's always sunny in {city}!"
  31. @mcp.tool()
  32. async def get_date() -> str:
  33.     """Get today's date."""
  34.     return datetime.now().strftime("%Y-%m-%d")
  35. @mcp.tool()
  36. async def execute_ssh_command_remote(hostname: str, command: str) -> str:
  37.     """Execute an SSH command on a remote host.
  38.    
  39.     Args:
  40.         hostname (str): The hostname of the remote host.
  41.         command (str): The SSH command to execute.
  42.     Returns:
  43.         str: The output of the SSH command.
  44.     """
  45.     try:
  46.         async with asyncssh.connect(hostname, username="rainux", connect_timeout=10) as conn:
  47.             result = await conn.run(command, timeout=10)
  48.             stdout = result.stdout
  49.             stderr = result.stderr
  50.             content = str(stdout if stdout else stderr)
  51.             return content
  52.     except Exception as e:
  53.         return f"Error executing command '{command}' on host '{hostname}': {str(e)}"
  54.    
  55. @mcp.tool()
  56. async def execute_command_local(command: str, timeout: int = 10) -> str:
  57.     """Execute a shell command locally.
  58.    
  59.     Args:
  60.         command (str): The shell command to execute.
  61.         timeout (int): Timeout in seconds for command execution. default is 10 seconds.
  62.     Returns:
  63.         str: The output of the shell command.
  64.     """
  65.     try:
  66.         proc = await asyncio.create_subprocess_shell(
  67.             command,
  68.             stdout=asyncio.subprocess.PIPE,
  69.             stderr=asyncio.subprocess.PIPE
  70.         )
  71.         stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout)
  72.         stdout_str = stdout.decode().strip()
  73.         stderr_str = stderr.decode().strip()
  74.         # content = stdout.decode() if stdout else stderr.decode()
  75.         if stdout_str:
  76.             return f"Stdout: {stdout_str}"
  77.         elif stderr_str:
  78.             return f"Stderr: {stderr_str}"
  79.         else:
  80.             return "Command executed successfully with no output"
  81.     except asyncio.TimeoutError:
  82.         if proc and not proc.returncode:
  83.             try:
  84.                 proc.terminate()
  85.                 await proc.wait()
  86.             except:
  87.                 pass
  88.         return f"Error: Command '{command}' timed out after {timeout} seconds"
  89.     except Exception as e:
  90.         return f"Error executing command '{command}': {str(e)}"
  91. if __name__ == "__main__":
  92.     mcp.run(transport="http", host="localhost", port=8001, show_banner=False)
复制代码
因为使用http协议,所以运行client前要先运行server,顺带测试下能否正常启动。
MCP Client

FastMCP的client写法与mcp官方sdk用法大致上也差不多,但在一些细节上更加友好。
  1. """
  2. MCP客户端示例程序
  3. 该程序演示了如何使用MCP协议与服务器进行交互,并通过LLM处理用户查询。
  4. """
  5. import asyncio
  6. import json
  7. import readline  # For enhanced input editing
  8. import traceback
  9. from typing import cast
  10. from openai.types.chat import ChatCompletionMessageFunctionToolCall
  11. from fastmcp import Client
  12. from openai import AsyncOpenAI
  13. from pkg.config import cfg
  14. from pkg.log import logger
  15. class MCPHost:
  16.     """MCP主机类,用于管理与MCP服务器的连接和交互"""
  17.    
  18.     def __init__(self, server_uri: str):
  19.         """
  20.         初始化MCP客户端
  21.         
  22.         Args:
  23.             server_uri (str): MCP服务器的URI地址
  24.         """
  25.         # 初始化MCP客户端连接
  26.         self.mcp_client: Client = Client(server_uri)
  27.         # 初始化异步OpenAI客户端用于与LLM交互
  28.         self.llm = AsyncOpenAI(
  29.             base_url=cfg.llm_base_url,
  30.             api_key=cfg.llm_api_key,
  31.         )
  32.         # 存储对话历史消息
  33.         self.messages = []
  34.     async def close(self):
  35.         """关闭MCP客户端连接"""
  36.         if self.mcp_client:
  37.             await self.mcp_client.close()
  38.     async def process_query(self, query: str) -> str:
  39.         """Process a user query by interacting with the MCP server and LLM.
  40.         
  41.         Args:
  42.             query (str): The user query to process.
  43.         Returns:
  44.             str: The response from the MCP server.
  45.         """
  46.         # 将用户查询添加到消息历史中
  47.         self.messages.append({
  48.             "role": "user",
  49.             "content": query,
  50.         })
  51.         # 使用异步上下文管理器确保MCP客户端连接正确建立和关闭
  52.         async with self.mcp_client:
  53.             # 从MCP服务器获取可用工具列表
  54.             tools = await self.mcp_client.list_tools()
  55.             # 构造LLM可以理解的工具格式
  56.             available_tools = []
  57.             # 将MCP工具转换为OpenAI格式
  58.             for tool in tools:
  59.                 available_tools.append({
  60.                     "type": "function",
  61.                     "function": {
  62.                         "name": tool.name,
  63.                         "description": tool.description,
  64.                         "parameters": tool.inputSchema,
  65.                     }
  66.                 })
  67.             logger.info(f"Available tools: {[tool['function']['name'] for tool in available_tools]}")
  68.             # 调用LLM,传入对话历史和可用工具
  69.             resp = await self.llm.chat.completions.create(
  70.                 model=cfg.llm_model,
  71.                 messages=self.messages,
  72.                 tools=available_tools,
  73.                 temperature=0.3,
  74.             )
  75.             # 存储最终响应文本
  76.             final_text = []
  77.             # 获取LLM的首个响应消息
  78.             message = resp.choices[0].message
  79.             # 如果响应包含直接内容,则添加到结果中
  80.             if hasattr(message, "content") and message.content:
  81.                 final_text.append(message.content)
  82.             # 循环处理工具调用,直到没有更多工具调用为止
  83.             while message.tool_calls:
  84.                 # 遍历所有工具调用
  85.                 for tool_call in message.tool_calls:
  86.                     # 确保工具调用有函数信息
  87.                     if not hasattr(tool_call, "function"):
  88.                         continue
  89.                     # 类型转换以获取函数调用详情
  90.                     function_call = cast(ChatCompletionMessageFunctionToolCall, tool_call)
  91.                     function = function_call.function
  92.                     tool_name = function.name
  93.                     # 解析函数参数
  94.                     tool_args = json.loads(function.arguments)
  95.                     # 检查MCP客户端是否已连接
  96.                     if not self.mcp_client.is_connected():
  97.                         raise RuntimeError("Session not initialized. Cannot call tool.")
  98.                     
  99.                     # 调用MCP服务器上的指定工具
  100.                     result = await self.mcp_client.call_tool(tool_name, tool_args)
  101.                     # 将助手的工具调用添加到消息历史中
  102.                     self.messages.append({
  103.                         "role": "assistant",
  104.                         "tool_calls": [
  105.                             {
  106.                                 "id": tool_call.id,
  107.                                 "type": "function",
  108.                                 "function": {
  109.                                     "name": function.name,
  110.                                     "arguments": function.arguments
  111.                                 }
  112.                             }
  113.                         ]
  114.                     })
  115.                     # 将工具调用结果添加到消息历史中
  116.                     self.messages.append({
  117.                         "role": "tool",
  118.                         "tool_call_id":tool_call.id,
  119.                         "content": str(result.content) if result.content else ""
  120.                     })
  121.                
  122.                 # 基于工具调用结果再次调用LLM
  123.                 final_resp = await self.llm.chat.completions.create(
  124.                     model=cfg.llm_model,
  125.                     messages=self.messages,
  126.                     tools=available_tools,
  127.                     temperature=0.3,
  128.                 )
  129.                 # 更新消息为最新的LLM响应
  130.                 message = final_resp.choices[0].message
  131.                 # 如果响应包含内容,则添加到最终结果中
  132.                 if message.content:
  133.                     final_text.append(message.content)
  134.             
  135.             # 返回连接后的完整响应
  136.             return "\n".join(final_text)
  137.     async def chat_loop(self):
  138.         """主聊天循环,处理用户输入并显示响应"""
  139.         print("Welcome to the MCP chat! Type 'quit' to exit.")
  140.         # 持续处理用户输入直到用户退出
  141.         while True:
  142.             try:
  143.                 # 获取用户输入
  144.                 query = input("You: ").strip()
  145.                 # 检查退出命令
  146.                 if query.lower() == "quit":
  147.                     print("Exiting chat. Goodbye!")
  148.                     break
  149.                 # 跳过空输入
  150.                 if not query:
  151.                     continue
  152.                 # 处理用户查询并获取响应
  153.                 resp = await self.process_query(query)
  154.                 print(f"Assistant: {resp}")
  155.             
  156.             # 捕获并记录聊天循环中的任何异常
  157.             except Exception as e:
  158.                 logger.error(f"Error in chat loop: {str(e)}")
  159.                 logger.error(traceback.format_exc())
  160. async def main():
  161.     """主函数,程序入口点"""
  162.     # 创建MCP主机实例
  163.     client = MCPHost(server_uri="http://localhost:8001/mcp")
  164.     try:
  165.         # 启动聊天循环
  166.         await client.chat_loop()
  167.     except Exception as e:
  168.         # 记录主程序中的任何异常
  169.         logger.error(f"Error in main: {str(e)}")
  170.         logger.error(traceback.format_exc())
  171.     finally:
  172.         # 确保客户端连接被正确关闭
  173.         await client.close()
  174.    
  175. if __name__ == "__main__":
  176.     # 运行主程序
  177.     asyncio.run(main())
复制代码
FastMCP的客户端API设计更加直观,特别是在连接管理和工具调用方面,代码更简洁易懂。
client运行输出:
  1. Welcome to the MCP chat! Type 'quit' to exit.
  2. You: 今天的日期是什么
  3. Assistant: 今天的日期是2025年9月13日。
  4. You: 检查下 tx 服务器和本地的内存占用情况
  5. Assistant: 以下是 tx 服务器和本地的内存占用情况:
  6. ### tx 服务器
  7.                total        used        free      shared  buff/cache   available
  8. Mem:           3.7Gi       2.2Gi       207Mi       142Mi       1.7Gi       1.5Gi
  9. Swap:             0B          0B          0B
  10. ### 本地
  11.                total        used        free      shared  buff/cache   available
  12. Mem:           62Gi        14Gi        38Gi       487Mi        10Gi        48Gi
  13. Swap:         3.8Gi          0B       3.8Gi
  14. 从这些信息中可以看出,tx 服务器的内存使用较高,而本地系统仍有较多可用内存。如果需要进一步分析或采取措施,请告诉我!
  15. You: 再查下硬盘
  16. Assistant: 已检查 tx 服务器和本地的内存及硬盘占用情况。以下是总结:
  17. ### tx 服务器
  18. - **内存占用**:
  19.   - 总内存: 3.7 Gi
  20.   - 已用内存: 2.2 Gi
  21.   - 可用内存: 1.5 Gi
  22. - **硬盘使用**:
  23.   - 根目录 `/`: 总大小 69G,已用 17G,可用 53G,使用率 24%
  24. ### 本地
  25. - **内存占用**:
  26.   - 总内存: 62 Gi
  27.   - 已用内存: 14 Gi
  28.   - 可用内存: 48 Gi
  29. - **硬盘使用**:
  30.   - 根目录 `/`: 总大小 234G,已用 30G,使用率 14%
  31.   - `/home`: 总大小 676G,已用 197G,使用率 31%
  32. 如果需要进一步操作,请告知!
  33. You: quit
  34. Exiting chat. Goodbye!
复制代码
可以看到,FastMCP的基本运行逻辑是正常的,跟使用MCP官方SDK相差不大,而且还更简洁一点。
小结

使用FastMCP与使用mcp官方sdk相比,整体体验更加友好。FastMCP不仅保持了与官方SDK的兼容性,还在API设计上做了优化,使得代码更加简洁易懂。后续博客中我们会继续使用FastMCP来介绍Sampling、Elicitation等MCP的高级功能。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册