找回密码
 立即注册
首页 业界区 业界 [MCP][02]快速入门MCP开发

[MCP][02]快速入门MCP开发

钱艷芳 5 天前
前言

很多文档和博客都只介绍如何开发MCP Server,然后集成到VS Code或者Cursor等程序,很少涉及如何开发MCP Host和MCP Client。如果你想要在自己的服务中集成完整的MCP功能,光看这些是远远不够的。所以本文及后续的MCP系列文章都会带你深入了解如何开发MCP Client,让你真正掌握这项技术。
准备开发环境

MCP官方SDK主要支持Python和TypeScript,当然也有其他语言的实现,不过我这里就以Python为例了。我的Python版本是3.13.5,但其实只要高于3.11应该都没问题。
我个人推荐使用uv来管理依赖,当然你也可以用传统的pip。Python SDK有官方的mcp包和社区的FastMCP包。官方SDK其实也内置了FastMCP,不过是v1版本,而FastMCP官网已经更新到了v2版本。作为学习,两个都装上试试也无妨。
  1. # 使用 uv
  2. uv add mcp fastmcp
  3. # 使用 pip
  4. python -m pip install mcp fastmcp
复制代码
第一个MCP项目:你好,MCP世界!

在第一个MCP项目中,我们实现一个简单的MCP Client和MCP Server,但还没集成LLM。在这个阶段,Client调用Server的tool或resource都需要手动指定。
MCP Server

下面的MCP Server示例代码定义了一些prompts、resources和tools。这里有个小贴士:函数参数的类型注解、返回类型和docstring都一定要写清楚,否则后续集成LLM时,LLM就无法正确理解如何调用你的工具了。
这段Server可以通过stdio方式被Client调用。在正式让Client调用之前,建议你先手动运行一下Server,测试它能否正常启动,避免Client启动时报一堆让人摸不着头脑的错误。
  1. from mcp.server.fastmcp import FastMCP
  2. from datetime import datetime
  3. import asyncssh
  4. from typing import TypeAlias, Union
  5. mcp = FastMCP("custom")
  6. @mcp.prompt()
  7. def greet_user(name: str, style: str = "formal") -> str:
  8.     """Greet a user with a specified style."""
  9.     if style == "formal":
  10.         return f"Good day, {name}. How do you do?"
  11.     elif style == "friendly":
  12.         return f"Hey {name}! What's up?"
  13.     elif style == "casual":
  14.         return f"Yo {name}, how's it going?"
  15.     else:
  16.         return f"Hello, {name}!"
  17. @mcp.resource("greeting://{name}")
  18. def greeting_resource(name: str) -> str:
  19.     """A simple greeting resource."""
  20.     return f"Hello, {name}!"
  21. @mcp.resource("config://app")
  22. def get_config() -> str:
  23.     """Static configuration data"""
  24.     return "App configuration here"
  25. @mcp.tool()
  26. def add(a: int, b: int) -> int:
  27.     """Add two numbers"""
  28.     return a + b
  29. @mcp.tool()
  30. def multiply(a: int, b: int) -> int:
  31.     """Multiply two numbers"""
  32.     return a * b
  33. Number: TypeAlias = Union[int, float]
  34. @mcp.tool()
  35. def is_greater_than(a: Number, b: Number) -> Number:
  36.     """Check if a is greater than b"""
  37.     return a > b
  38. @mcp.tool()
  39. async def get_weather(city: str) -> str:  
  40.     """Get weather for a given city."""
  41.     return f"It's always sunny in {city}!"
  42. @mcp.tool()
  43. async def get_date() -> str:
  44.     """Get today's date."""
  45.     return datetime.now().strftime("%Y-%m-%d")
  46. @mcp.tool()
  47. async def execute_ssh_command_remote(hostname: str, command: str) -> str:
  48.     """Execute an SSH command on a remote host.
  49.    
  50.     Args:
  51.         hostname (str): The hostname of the remote host.
  52.         command (str): The SSH command to execute.
  53.     Returns:
  54.         str: The output of the SSH command.
  55.     """
  56.     async with asyncssh.connect(hostname, username="rainux", connect_timeout=10) as conn:
  57.         result = await conn.run(command, timeout=10)
  58.         stdout = result.stdout
  59.         stderr = result.stderr
  60.         content = str(stdout if stdout else stderr)
  61.         return content
  62. if __name__ == "__main__":
  63.     mcp.run(transport="stdio")
复制代码
MCP Client

Client通过STDIO方式调用MCP Server,server_params中指定了如何运行Server,包括python解释器路径、Server文件名和运行位置。需要注意的是,Client启动时也会启动Server,如果Server报错,Client也会跟着无法启动。
  1. import asyncio
  2. from pathlib import Path
  3. from pydantic import AnyUrl
  4. from mcp import ClientSession, StdioServerParameters, types
  5. from mcp.client.stdio import stdio_client
  6. server_params = StdioServerParameters(
  7.     command=str(Path(__file__).parent / ".venv" / "bin" / "python"),
  8.     args=[str(Path(__file__).parent / "demo1-server.py")],
  9.     cwd=str(Path(__file__).parent),
  10. )
  11. async def run():
  12.     async with stdio_client(server_params) as (read, write):
  13.         async with ClientSession(read, write) as session:
  14.             # Initialize the connection
  15.             await session.initialize()
  16.             # List available prompts
  17.             prompts = await session.list_prompts()
  18.             print(f"Available prompts: {[p.name for p in prompts.prompts]}")
  19.             # Get a prompt (greet_user prompt from fastmcp_quickstart)
  20.             if prompts.prompts:
  21.                 prompt = await session.get_prompt("greet_user", arguments={"name": "Alice", "style": "friendly"})
  22.                 print(f"Prompt result: {prompt.messages[0].content}")
  23.             # List available resources
  24.             resources = await session.list_resources()
  25.             print(f"Available resources: {[r.uri for r in resources.resources]}")
  26.             # List available tools
  27.             tools = await session.list_tools()
  28.             print(f"Available tools: {[t.name for t in tools.tools]}")
  29.             # Read a resource (greeting resource from fastmcp_quickstart)
  30.             resource_content = await session.read_resource(AnyUrl("greeting://World"))
  31.             content_block = resource_content.contents[0]
  32.             if isinstance(content_block, types.TextResourceContents):
  33.                 print(f"Resource content: {content_block.text}")
  34.             # Call a tool (add tool from fastmcp_quickstart)
  35.             result = await session.call_tool("add", arguments={"a": 5, "b": 3})
  36.             result_unstructured = result.content[0]
  37.             if isinstance(result_unstructured, types.TextContent):
  38.                 print(f"Tool result: {result_unstructured.text}")
  39.             result_structured = result.structuredContent
  40.             print(f"Structured tool result: {result_structured}")
  41. if __name__ == "__main__":
  42.     asyncio.run(run())
复制代码
运行Client,输出如下:
  1. Processing request of type ListPromptsRequest
  2. Available prompts: ['greet_user']
  3. Processing request of type GetPromptRequest
  4. Prompt result: type='text' text="Hey Alice! What's up?" annotations=None meta=None
  5. Processing request of type ListResourcesRequest
  6. Available resources: [AnyUrl('config://app')]
  7. Processing request of type ListToolsRequest
  8. Available tools: ['add', 'multiply', 'get_weather', 'get_date', 'execute_ssh_command_remote']
  9. Processing request of type ReadResourceRequest
  10. Resource content: Hello, World!
  11. Processing request of type CallToolRequest
  12. Tool result: 8
  13. Structured tool result: {'result': 8}
复制代码
可以看到,Client成功地调用了Server上的各种功能,包括获取提示、读取资源和调用工具。
使用streamable-http远程调用:让MCP飞起来!

上面的例子中,Client通过STDIO方式在本地调用Server。现在我们稍作修改,让它可以通过HTTP远程调用Server,这样就更加灵活了。
MCP Server

只列出修改的部分:
  1. mcp = FastMCP("custom", host="localhost", port=8001)
  2. if __name__ == "__main__":
  3.     mcp.run(transport="streamable-http")
复制代码
修改完成后,启动Server,它会监听在localhost:8001地址上,就像一个小小的Web服务(其实就是个Web服务,暴露的api为/mcp)。
MCP Client

同样只列出修改的部分。Client需要指定MCP Server的地址。streamablehttp_client返回的第三个参数get_session_id用于会话管理,大多数情况下你不需要直接使用它,所以在一些文档中这里会用_来占位。
  1. from mcp.client.streamable_http import streamablehttp_client
  2. server_uri = "http://localhost:8001/mcp"
  3. async def main():
  4.     async with streamablehttp_client(server_uri) as (read, write, get_session_id):
  5.         # 获取当前会话ID
  6.         session_id = get_session_id()
  7.         print(f"Session ID before initialization: {session_id}")
  8.         
  9.         async with ClientSession(read, write) as session:
  10.             # Initialize the connection
  11.             await session.initialize()
  12.             
  13.             # 初始化后再次获取会话ID
  14.             session_id = get_session_id()
  15.             print(f"Session ID after initialization: {session_id}")
复制代码
client运行输出:
  1. Session ID before initialization: None
  2. Session ID after initialization: 60ce4204b907469e9eb46e7e01df040d
  3. Available prompts: ['greet_user']
  4. Prompt result: type='text' text="Hey Alice! What's up?" annotations=None meta=None
  5. Available resources: [AnyUrl('config://app')]
  6. Available tools: ['add', 'multiply', 'get_weather', 'get_date', 'execute_ssh_command_remote']
  7. Resource content: Hello, World!
  8. Tool result: 8
  9. Structured tool result: {'result': 8}
复制代码
现在我们的MCP应用已经可以通过网络进行远程调用了,架构变得更加灵活。
集成LLM:让AI自己做决定!

前面两个示例中,我们都需要在Client中手动控制调用Server的tool,这在实际应用中显然是不现实的。我们需要集成LLM,让AI自己决定该调用哪个工具。
MCP Server

Server端不需要做任何变更,Client还是通过HTTP方式调用我们之前创建的Server。
MCP Client

这里我们选用阿里的通义千问(Qwen)。Qwen的API Key可以自行申请,氪个5块钱就够个人开发用很久了。为了便于后续开发,我把配置功能单独放到了一个模块里,下面代码中直接使用了,相关模块放在"补充"部分。
  1. """
  2. MCP (Model Context Protocol) 客户端示例
  3. 该客户端演示了如何使用 MCP 协议与 MCP 服务器进行交互,并通过 LLM 调用服务器提供的工具。
  4. 工作流程:
  5. 1. 连接到 MCP 服务器
  6. 2. 获取服务器提供的工具列表
  7. 3. 用户输入查询
  8. 4. 将查询发送给 LLM,LLM 可能会调用 MCP 服务器提供的工具
  9. 5. 执行工具调用并获取结果
  10. 6. 将结果返回给 LLM 进行最终回答
  11. """
  12. import asyncio
  13. # JSON 处理
  14. import json
  15. # 增强输入功能(在某些系统上提供命令历史等功能)
  16. import readline  # 引入readline模块用于增强python的input功能, Windows下的python标准库可能不包含
  17. # 异常追踪信息
  18. import traceback
  19. # 异步上下文管理器,用于资源管理
  20. from contextlib import AsyncExitStack
  21. # 类型提示支持
  22. from typing import List, Optional, cast
  23. # MCP 客户端会话和 HTTP 传输
  24. from mcp import ClientSession
  25. from mcp.client.streamable_http import streamablehttp_client
  26. # OpenAI 异步客户端,用于与 LLM 通信
  27. from openai import AsyncOpenAI
  28. # OpenAI 聊天完成相关的类型定义
  29. from openai.types.chat import (ChatCompletionAssistantMessageParam,
  30.                                ChatCompletionMessageFunctionToolCall,
  31.                                ChatCompletionMessageParam,
  32.                                ChatCompletionMessageToolCall,
  33.                                ChatCompletionToolMessageParam,
  34.                                ChatCompletionToolParam,
  35.                                ChatCompletionUserMessageParam)
  36. # 项目配置和日志模块
  37. from pkg.config import cfg
  38. from pkg.log import logger
  39. class MCPClient:
  40.     """
  41.     MCP 客户端类,负责管理与 MCP 服务器的连接和交互
  42.     """
  43.    
  44.     def __init__(self):
  45.         """
  46.         初始化 MCP 客户端
  47.         """
  48.         # 客户端会话,初始为空
  49.         self.session: Optional[ClientSession] = None
  50.         # 异步上下文管理栈,用于管理异步资源的生命周期
  51.         self.exit_stack = AsyncExitStack()
  52.         # OpenAI 异步客户端,用于与 LLM 通信
  53.         self.client = AsyncOpenAI(
  54.             base_url=cfg.llm_base_url,
  55.             api_key=cfg.llm_api_key,
  56.         )
  57.     async def connect_to_server(self, server_uri: str):
  58.         """
  59.         连接到 MCP 服务器
  60.         
  61.         Args:
  62.             server_uri (str): MCP 服务器的 URI
  63.         """
  64.         # 创建 Streamable HTTP 传输连接
  65.         http_transport = await self.exit_stack.enter_async_context(
  66.             streamablehttp_client(server_uri)
  67.         )
  68.         # 获取读写流
  69.         self.read, self.write, _ = http_transport
  70.         # 创建并初始化客户端会话
  71.         self.session = await self.exit_stack.enter_async_context(
  72.             ClientSession(self.read, self.write)
  73.         )
  74.         # 初始化会话
  75.         await self.session.initialize()
  76.         # 检查会话是否成功初始化
  77.         if self.session is None:
  78.             raise RuntimeError("Failed to initialize session")
  79.             
  80.         # 获取服务器提供的工具列表
  81.         response = await self.session.list_tools()
  82.         tools = response.tools
  83.         logger.info(f"\nConnected to server with tools: {[tool.name for tool in tools]}")
  84.     async def process_query(self, query: str) -> str:
  85.         """
  86.         处理用户查询
  87.         
  88.         Args:
  89.             query (str): 用户的查询
  90.             
  91.         Returns:
  92.             str: 处理结果
  93.         """
  94.         # 初始化消息历史,包含用户的查询
  95.         messages: List[ChatCompletionMessageParam] = [
  96.             ChatCompletionUserMessageParam(
  97.                 role="user",
  98.                 content=query
  99.             )
  100.         ]
  101.         
  102.         # 确保会话已初始化
  103.         if self.session is None:
  104.             raise RuntimeError("Session not initialized. Please connect to server first.")
  105.             
  106.         # 获取服务器提供的工具列表
  107.         response = await self.session.list_tools()
  108.         
  109.         # 构建工具列表,处理可能为None的字段
  110.         # 这些工具将被传递给 LLM,以便 LLM 知道可以调用哪些工具
  111.         available_tools: List[ChatCompletionToolParam] = []
  112.         for tool in response.tools:
  113.             tool_def: ChatCompletionToolParam = {
  114.                 "type": "function",
  115.                 "function": {
  116.                     "name": tool.name,
  117.                     "description": tool.description or "",
  118.                     "parameters": tool.inputSchema or {}
  119.                 }
  120.             }
  121.             available_tools.append(tool_def)
  122.         logger.info(f"Available tools: {available_tools}")
  123.         # 调用 LLM 进行聊天完成
  124.         response = await self.client.chat.completions.create(
  125.             model=cfg.llm_model,
  126.             messages=messages,
  127.             tools=available_tools,
  128.         )
  129.         # 存储最终输出文本
  130.         final_text = []
  131.         # 获取 LLM 的响应消息
  132.         message = response.choices[0].message
  133.         final_text.append(message.content or "")
  134.         # 如果 LLM 要求调用工具,则处理工具调用
  135.         while message.tool_calls:
  136.             # 处理每个工具调用
  137.             for tool_call in message.tool_calls:
  138.                 # 确保我们处理的是正确的工具调用类型
  139.                 if hasattr(tool_call, 'function'):
  140.                     # 这是一个函数工具调用
  141.                     function_call = cast(ChatCompletionMessageFunctionToolCall, tool_call)
  142.                     function = function_call.function
  143.                     tool_name = function.name
  144.                     # 解析工具参数
  145.                     tool_args = json.loads(function.arguments)
  146.                 else:
  147.                     # 跳过不支持的工具调用类型
  148.                     continue
  149.                 # 执行工具调用
  150.                 if self.session is None:
  151.                     raise RuntimeError("Session not initialized. Cannot call tool.")
  152.                     
  153.                 # 调用 MCP 服务器上的工具
  154.                 result = await self.session.call_tool(tool_name, tool_args)
  155.                 final_text.append(f"[Calling tool {tool_name} with args {tool_args}]")
  156.                 # 将工具调用和结果添加到消息历史
  157.                 # 这样 LLM 可以知道它之前调用了哪些工具
  158.                 assistant_msg: ChatCompletionAssistantMessageParam = {
  159.                     "role": "assistant",
  160.                     "tool_calls": [
  161.                         {
  162.                             "id": tool_call.id,
  163.                             "type": "function",
  164.                             "function": {
  165.                                 "name": tool_name,
  166.                                 "arguments": json.dumps(tool_args)
  167.                             }
  168.                         }
  169.                     ]
  170.                 }
  171.                 messages.append(assistant_msg)
  172.                
  173.                 # 添加工具调用结果到消息历史
  174.                 tool_msg: ChatCompletionToolMessageParam = {
  175.                     "role": "tool",
  176.                     "tool_call_id": tool_call.id,
  177.                     "content": str(result.content) if result.content else ""
  178.                 }
  179.                 messages.append(tool_msg)
  180.             # 将工具调用的结果交给 LLM,让 LLM 生成最终回答
  181.             response = await self.client.chat.completions.create(
  182.                 model=cfg.llm_model,
  183.                 messages=messages,
  184.                 tools=available_tools
  185.             )
  186.             # 获取新的响应消息
  187.             message = response.choices[0].message
  188.             if message.content:
  189.                 final_text.append(message.content)
  190.         # 返回最终结果
  191.         return "\n".join(final_text)
  192.    
  193.     async def chat_loop(self):
  194.         """
  195.         运行交互式聊天循环
  196.         """
  197.         print("\nMCP Client Started!")
  198.         print("Type your queries or 'quit' to exit.")
  199.         # 持续接收用户输入
  200.         while True:
  201.             try:
  202.                 # 获取用户输入
  203.                 query = input("\nQuery: ").strip()
  204.                 # 检查是否退出
  205.                 if query.lower() == 'quit':
  206.                     break
  207.                 # 忽略空输入
  208.                 if not query:
  209.                     continue
  210.                 # 处理用户查询并输出结果
  211.                 response = await self.process_query(query)
  212.                 print("\n" + response)
  213.             # 异常处理
  214.             except Exception as e:
  215.                 print(f"\nError: {str(e)}")
  216.                 print(traceback.format_exc())
  217.     async def cleanup(self):
  218.         """
  219.         清理资源
  220.         """
  221.         await self.exit_stack.aclose()
  222. async def main():
  223.     """
  224.     主函数
  225.     """
  226.     # 创建 MCP 客户端实例
  227.     client = MCPClient()
  228.     try:
  229.         # 连接到 MCP 服务器
  230.         await client.connect_to_server("http://localhost:8001/mcp")
  231.         # 运行聊天循环
  232.         await client.chat_loop()
  233.     except Exception as e:
  234.         print(f"Error: {str(e)}")
  235.     finally:
  236.         # 清理资源
  237.         await client.cleanup()
  238. # 程序入口点
  239. if __name__ == "__main__":
  240.     asyncio.run(main())
复制代码
client运行输出:
  1. MCP Client Started!
  2. Type your queries or 'quit' to exit.
  3. Query: 今天的日期是什么
  4. [Calling tool get_date with args {}]
  5. 今天的日期是2025年9月13日。
  6. Query: 合肥的天气怎么样?
  7. [Calling tool get_weather with args {'city': '合肥'}]
  8. 合肥的天气总是阳光明媚!
  9. Query: 0.11比0.9大吗
  10. [Calling tool is_greater_than with args {'a': 0.11, 'b': 0.9}]
  11. 0.11 不比 0.9 大。0.11 小于 0.9。
  12. Query: quit
复制代码
现在AI可以自己决定调用哪个工具了。当你问"今天的日期是什么"时,它会自动调用get_date工具;当你问"合肥的天气怎么样"时,它会自动调用get_weather工具。这才是真正的智能!
小结

通过这篇文章,我们从零开始构建了一个完整的MCP应用,涵盖了从基础的Client-Server通信到集成LLM的全过程。我们学习了:

  • 如何搭建MCP开发环境
  • 如何创建MCP Server并定义tools、resources和prompts
  • 如何编写MCP Client并通过stdio和HTTP两种方式与Server通信
  • 如何集成LLM,让AI自主决定调用哪个工具
整个过程就像搭积木一样,每一步都有其特定的作用:

  • Server负责提供功能(工具和资源)
  • Client负责协调和调用这些功能
  • LLM负责智能决策,决定何时以及如何使用这些功能
这种架构的优势在于功能扩展非常灵活。当你需要添加新功能时,只需要在Server端添加新的tools或resources,Client和LLM会自动发现并使用它们,而不需要修改Client端的代码。
MCP真正实现了"上下文协议"的概念,让AI可以像人类一样访问和操作各种工具和资源,这是迈向更强大AI应用的重要一步。接下来你可以尝试添加更多有趣的工具,比如文件操作、数据库查询、API调用等,让你的AI助手变得更加强大!
补充

配置模块

pkg/config.py
  1. import json
  2. from pathlib import Path
  3. class Config:
  4.     def __init__(self):
  5.         p = Path(__file__).parent.parent / "conf" / "config.json"
  6.         if not p.exists():
  7.             raise FileNotFoundError(f"Config file not found: {p}")
  8.         self.data = self.read_json(str(p))
  9.     def read_json(self, filepath: str) -> dict:
  10.         with open(filepath, "r") as f:
  11.             return json.load(f)
  12.         
  13.     @property
  14.     def llm_model(self) -> str:
  15.         return self.data["llm"]["model"]
  16.    
  17.     @property
  18.     def llm_api_key(self):
  19.         return self.data["llm"]["api_key"]
  20.    
  21.     @property
  22.     def llm_base_url(self) -> str:
  23.         return self.data["llm"]["base_url"]
  24.    
  25.     @property
  26.     def server_host(self) -> str:
  27.         return self.data["server"]["host"]
  28.    
  29.     @property
  30.     def server_port(self) -> int:
  31.         return self.data["server"]["port"]
  32.    
  33. cfg = Config()
复制代码
配置文件conf/config.json
  1. {
  2.     "llm": {
  3.         "model": "qwen-plus",
  4.         "base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
  5.         "api_key": "your token"
  6.     },
  7.     "server": {
  8.         "host": "127.0.0.1",
  9.         "port": 8000
  10.     }
  11. }
复制代码
日志模块

pkg/log.py
  1. import logging
  2. import sys
  3. def set_formatter():
  4.     """设置formatter"""
  5.     fmt = "%(asctime)s | %(name)s | %(levelname)s | %(filename)s:%(lineno)d | %(funcName)s | %(message)s"
  6.     datefmt = "%Y-%m-%d %H:%M:%S"
  7.     return logging.Formatter(fmt, datefmt=datefmt)
  8. def set_stream_handler():
  9.     return logging.StreamHandler(sys.stdout)
  10. def set_file_handler():
  11.     return logging.FileHandler("app.log", mode="a", encoding="utf-8")
  12. def get_logger(name: str = "mylogger", level=logging.DEBUG):
  13.     logger = logging.getLogger(name)
  14.     formatter = set_formatter()
  15.     # handler = set_stream_handler()
  16.     handler = set_file_handler()
  17.     handler.setFormatter(formatter)
  18.     logger.addHandler(handler)
  19.     logger.setLevel(level)
  20.     return logger
  21. logger = get_logger()
复制代码
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

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