如何使用MCP开发一个客户端和服务端
一、MCP和API以及Function Call核心概念对比
特性 | API | Function Call | MCP (Model Context Protocol) |
定位 | 通用应用程序接口 | 大模型原生扩展能力 | 标准化模型-服务交互协议 |
耦合度 | 与具体服务绑定 | 与模型强绑定 (如 GPT-4-turbo) | 与模型解耦,跨平台通用 |
交互模式 | 直接请求-响应 | 模型生成结构化调用建议 | JSON-RPC 2.0 标准化通信 |
典型场景 | 数据集成、微服务通信 | 简单实时操作 (天气/订单查询) | 复杂异步任务 & 跨系统整合 |
二、 MCP 协议
1. 什么是MCP协议
模型上下文协议(Model Context Protocol)是一种专为大语言模型设计的标准化协议,它允许LLM以安全、一致的方式与外部系统交互。MCP协议常被描述为"AI的USB-C接口",提供了一种统一的方式连接LLM与它们可以使用的资源。
MCP协议的核心功能包括:
- • 资源(Resources):类似于GET端点,用于将信息加载到LLM的上下文中
- • 工具(Tools):类似于POST端点,用于执行代码或产生副作用
- • 提示(Prompts):可重用的LLM交互模板
- • 上下文(Context):提供额外的交互功能,如日志记录和进度报告
2. 核心价值
- • 标准化:统一 AI 与外部服务的交互格式,解决工具碎片化问题
- • 解耦设计:模型无需硬编码 API 逻辑,通过声明式函数描述调用服务
- • 异步支持:适用于多步骤工作流(如爬取数据→分析→存储)
3. 工作流程
MCP 大概的工作方式: Claude Desktop、Cursor 这些工具,在内部实现了 MCP Client,然后MCP Client 通过标准的 MCP 协议和 MCP Server 进行交互,由各种三方开发者提供的 MCP Server 负责实现各种和三方资源交互的逻辑,比如访问数据库、浏览器、本地文件,最终再通过 标准的 MCP 协议返回给 MCP Client,最终给用户进行展示。
下图是一个通过查询天气来简单展示其对应的工作方式:
3. 代码实现mcp客户端和服务端
现在python编写mcp server和mcp client的有两个分别是FastMCP 和MCP,其中MCP是官方的pythonsdk,这两个之间的关系是官方收编了FastMCP的第一个版本的包,但官方集成的是 fastmcp 的 v1.0 版本。然而,jlowin 继续开发 fastmcp,还发布了 v2.0 版本,其中包含代理和客户端采样等新功能。以下的演示以官方版本MCP为例,
安装:uv add "mcp[cli]”
或者pip install "mcp[cli]”
(1) MCP 服务端
from mcp.server.fastmcp import FastMCP from mcp.server.fastmcp.prompts import base # mcp = FastMCP(name="demo",host="127.0.0.1",port=8256,sse_path="/sse") ### 启动方式为sse时使用 mcp = FastMCP() @mcp.tool() def add_2_numbers(a: int, b: int) -> int: """两个数字相加""" return a + b @mcp.resource("config://app") def get_config() -> str: """Static configuration data""" return "App configuration here" @mcp.prompt() def debug_error(error: str) -> list[base.Message]: return [ base.UserMessage("I'm seeing this error:"), base.UserMessage(error), base.AssistantMessage("I'll help debug that. What have you tried so far?"), @mcp.tool() def multiply_2_numbers(a: int, b: int): """两个数字相乘""" return a * b if __name__ == "__main__": # mcp.run(transport='sse') ## 启动方式为sse mcp.run(transport='stdio') ## 启动方式为stdio
解释:
- • Tools(工具)是MCP中最常用的功能之一,它允许LLM执行特定的操作或函数。使用
@mcp.tool()
装饰器可以轻松将Python函数转换为LLM可调用的工具: - • Resources(资源)用于向LLM提供数据和上下文信息。与工具不同,资源主要用于读取数据而非执行操作
- • Prompts(提示)允许您创建可重用的提示模板,这些模板可以被参数化并用于标准化LLM交互
简单验证服务端功能可以通过mcp dev server.py
进入界面检测
(2) MCP 客户端
MCP客户端一般分别按照服务端的stdio和sse分别写了两个,具体融合的最后修改一下即可。
- 1. STDIO客户端
import asyncio import json import re from contextlib import AsyncExitStack from typing import Optional from lxml import etree from mcp import ClientSession, StdioServerParameters, stdio_client from mcp.client.sse import sse_client from openai import AsyncOpenAI class Stdio_MCPClient(): def __init__(self,api_key, base_url, model): self.session: Optional[ClientSession] = None self.exit_stack = AsyncExitStack() self.client = AsyncOpenAI(api_key=api_key, base_url=base_url) self.model = model self.message = [] with open("MCP_Prompt.txt", "r", encoding="utf-8") as f: self.system_prompt = f.read() async def connect_to_stdio_server(self, mcp_name, command,args,env={}): server_params = StdioServerParameters( command=command, args=args, env=env ) stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params)) self.stdio,self.write = stdio_transport self.session = await self.exit_stack.enter_async_context(ClientSession(self.stdio, self.write)) await self.session.initialize() response = await self.session.list_tools() tools = response.tools print(f"成功链接到{mcp_name}服务,对应的tools:",[tool.name for tool in tools]) self.available_tools = [{ "type": "function", "function": { "name": tool.name, "description": tool.description, "parameters": tool.inputSchema } } for tool in response.tools] async def process_query(self, query: str, stream: bool = False): # self.message.append({"role": "system", "content": self.system_prompt}) self.message.append({"role": "user", "content": query}) response = await self.client.chat.completions.create( model=self.model, messages=self.message, tools=self.available_tools ) final_text = [] assistant_message = response.choices[0].message while assistant_message.tool_calls: for tool_call in assistant_message.tool_calls: tool_name = tool_call.function.name tool_args = json.loads(tool_call.function.arguments) result = await self.session.call_tool(tool_name, tool_args) print(f"calling tools {tool_name},wirh args {tool_args}") print("Result:", result.content[0].text) self.message.extend([ { "role": "assistant", "content": None, "tool_calls": [tool_call] }, { "role": "tool", "content": result.content[0].text, "tool_call_id": tool_call.id } ]) response = await self.client.chat.completions.create( model=self.model, messages=self.message, tools=self.available_tools, max_tokens=8048 ) assistant_message = response.choices[0].message content = assistant_message.content final_text.append(content) return "\n".join(final_text) async def chat_loop(self,stream_mode=True): self.message = [] while True: try: query = input("\nQuery: ").strip() if query.lower() == 'quit': break if query.strip() == '': continue response = await self.process_query(query, stream=stream_mode) print("\nAI:", response) except Exception as e: print(f"\nError: {str(e)}") async def cleanup(self): await self.exit_stack.aclose() async def main(): with open("config.json", "r") as f: config = json.load(f) client = Stdio_MCPClient(config["llm"]["api_key"], config["llm"]["base_url"], config["llm"]["model"]) try: env = {} await client.connect_to_stdio_server("testserver","python",["server.py",],{}) await client.chat_loop() except Exception as e: print(e) finally: await client.cleanup() if __name__ == '__main__': asyncio.run(main())
- 1. SSE客户端
import asyncio import json import re from contextlib import AsyncExitStack from typing import Optional from lxml import etree from mcp import ClientSession, StdioServerParameters, stdio_client from mcp.client.sse import sse_client from openai import AsyncOpenAI class SSE_MCPClient(): def __init__(self,api_key, base_url, model): self.session: Optional[ClientSession] = None self.exit_stack = AsyncExitStack() self.client = AsyncOpenAI(api_key=api_key, base_url=base_url) self.model = model self.message = [] with open("MCP_Prompt.txt", "r", encoding="utf-8") as f: self.system_prompt = f.read() async def connect_to_sse_server(self, mcp_name, server_url,headers=None): self.service = [{ "name": mcp_name, "url": server_url, "headers": headers }] sse_transport = await self.exit_stack.enter_async_context(sse_client(server_url, headers,timeout=30,sse_read_timeout=30)) self.sse,self.write = sse_transport self.session = await self.exit_stack.enter_async_context(ClientSession(self.sse, self.write)) await self.session.initialize() response = await self.session.list_tools() tools = response.tools print(f"成功链接到{mcp_name}服务,对应的tools:",[tool.name for tool in tools]) self.available_tools = [{ "type": "function", "function": { "name": tool.name, "description": tool.description, "parameters": tool.inputSchema } } for tool in response.tools] async def reconnect_sse_server(self): for service in self.service: mcp_name = service["name"] server_url = service["url"] headers = service.get("headers", None) sse_transport = await self.exit_stack.enter_async_context(sse_client(server_url, headers)) self.sse, self.write = sse_transport self.session = await self.exit_stack.enter_async_context(ClientSession(self.sse, self.write)) await self.session.initialize() print(f"重新成功链接到 {mcp_name} 服务") async def process_query(self, query: str, stream: bool = False): # self.message.append({"role": "system", "content": self.system_prompt}) self.message.append({"role": "user", "content": query}) response = await self.client.chat.completions.create( model=self.model, messages=self.message, tools=self.available_tools ) final_text = [] assistant_message = response.choices[0].message while assistant_message.tool_calls: for tool_call in assistant_message.tool_calls: tool_name = tool_call.function.name tool_args = json.loads(tool_call.function.arguments) await self.reconnect_sse_server() result = await self.session.call_tool(tool_name, tool_args) print(f"calling tools {tool_name},wirh args {tool_args}") print("Result:", result.content[0].text) self.message.extend([ { "role": "assistant", "content": None, "tool_calls": [tool_call] }, { "role": "tool", "content": result.content[0].text, "tool_call_id": tool_call.id } ]) response = await self.client.chat.completions.create( model=self.model, messages=self.message, tools=self.available_tools, max_tokens=8048 ) assistant_message = response.choices[0].message content = assistant_message.content final_text.append(content) return "\n".join(final_text) async def chat_loop(self,stream_mode=True): self.message = [] while True: try: query = input("\nQuery: ").strip() if query.lower() == 'quit': break if query.strip() == '': continue response = await self.process_query(query, stream=stream_mode) print("\nAI:", response) except Exception as e: print(f"\nError: {str(e)}") async def cleanup(self): await self.exit_stack.aclose() async def main(): with open("config.json", "r") as f: config = json.load(f) client = Stdio_MCPClient(config["llm"]["api_key"], config["llm"]["base_url"], config["llm"]["model"]) try: await client.connect_to_sse_server(mcp_name="test", server_url="https://127.0.0.1:7860/sse") await client.chat_loop() except Exception as e: print(e) finally: await client.cleanup() if __name__ == '__main__': asyncio.run(main())
注意: sse链接,我增加了一个reconnect_sse_server
函数,主要原因是sse链接过程中过2分钟会自然断开,不论什么办法都无法处理,因此增加这样一个操作。
(3)版本的自然更新
有了上面两种客户端的连接方法,自然而然结合两个就可以做到同时结合sse和stdio的方法只需要增加一个分别调用的方法即可,后续代码微微改动便可使用。
当然官方的MCP也是在不段更新的,看了官方有发布Streamable HTTP Transport ,这种方式在取代sse,以及通过with来启动执行服务的更新等等,一些简单的更新参考下面,更多更新可以前往github上看
from mcp.server.fastmcp import FastMCP # Stateful server (maintains session state) mcp = FastMCP("StatefulServer") # Stateless server (no session persistence) mcp = FastMCP("StatelessServer", stateless_http=True) # Stateless server (no session persistence, no sse stream with supported client) mcp = FastMCP("StatelessServer", stateless_http=True, json_response=True) # Run server with streamable_http transport mcp.run(transport="streamable-http")
其余高级用法可参考页面:https://github.com/modelcontextprotocol/python-sdk#advanced-usage
三、典型应用场景
1. MCP 适用场景
- • 企业系统整合 将 CRM/ERP 封装为 MCP 服务,供多个 Agent 安全调用
# MCP 连接数据库示例 @app.post("/mcp") def query_database(request: dict): if request["function"] == "get_user_orders": user_id = request["parameters"]["user_id"] # 执行SQL查询 (伪代码) return {"orders": db.query(f"SELECT * FROM orders WHERE user_id={user_id}")} - • 跨平台自动化 组合 GitHub + Slack 的 MCP 服务实现 CI/CD 流程:
# 自动化工作流:提交代码→构建→通知 def ci_cd_pipeline(): call_mcp("github", {"action": "pull_code", "repo": "my-app"}) build_result = call_mcp("jenkins", {"job": "build"}) call_mcp("slack", {"channel": "dev-team", "message": f"构建结果:{build_result}"})
2. Function Call 适用场景
# 简单实时查询(无需MCP) def get_stock_price(symbol: str): return yahoo_finance_api(symbol) # 注册函数到模型 functions = [{ "name": "get_stock_price", "parameters": {"symbol": {"type": "string"}} }] # 模型直接调用 response = model.generate("AAPL当前股价?", functions=functions) if response.function_call: print(get_stock_price(response.function_call.arguments["symbol"]))
3. 传统 API 调用
# 直接调用 REST API(无AI参与) import requests def fetch_weather(city: str): response = requests.get(f"https://api.weather.com/v1/{city}") return response.json()["temperature"]
四、技术选型建议
场景 | 推荐方案 | 原因 |
简单同步任务(天气/股价查询) | Function Call | 低延迟,与模型紧密集成 |
跨系统异步任务(数据分析流水线) | MCP | 标准化协议支持复杂工作流 |
企业内部系统暴露服务 | MCP | 统一认证 + 访问控制 |
第三方公共服务调用 | API + Function Call | 无需额外协议层 |
关键结论:MCP 的核心价值在于建立企业级 AI 基础设施。当系统需要连接多个异构数据源、要求严格的协议标准化或涉及长周期任务时,MCP 是优于 Function Call 的选择。
🌟 如果您对前沿科技、人工智能,尤其是多模态语言模型的应用前景充满好奇,那么这里就是您获取最新资讯、深入解析的绝佳平台。我们不仅分享创新技术,还探讨它们如何塑造我们的未来。
🔍 想要不错过任何一篇精彩内容,就请订阅我们的公众号吧!您的关注是我们持续探索和分享的动力。在这里,我们一起揭开AI的神秘面纱,见证科技如何让世界变得更加精彩。
