前言
之前的示例用的都是MCP的官方SDK(版本 1.14.0),简单使用还是没问题的,但对于Sampling、Elicitation这些相对高级的功能,官方没有提供Demo,而且因为比较新,网上也没搜到能用的案例。以我自己的水平折腾了一天也没捣鼓出来。在翻mcp源码时意外发现了其内置的FastMCP,顺藤摸瓜找到了FastMCP的官网,在官方文档中找到了相关用法。这里我们就用FastMCP来实现之前用mcp官方sdk做的功能,看看它有什么优势。
安装
截至本文日期的fastmcp版本为 2.12.2
bash
# uv
uv add fastmcp
# pip
python -m pip install fastmcp
MCP Server
MCP Server的写法跟之前使用mcp官方sdk差不多,只是导入FastMCP
的地方和运行配置不太一样。
python
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用法大致上也差不多,但在一些细节上更加友好。
python
"""
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的高级功能。