如何使用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,最终给用户进行展示。
下图是一个通过查询天气来简单展示其对应的工作方式:
External_Service
MCP_Server
AI_Model
User
External_Service
MCP_Server
AI_Model
User
"查询北京天气"
解析意图,生成MCP调用
{ "function": "get_weather", "params": {"city":"北京"} }
调用天气API
返回原始数据
{ "temperature": "22°C", "condition": "晴" }
"北京今天晴天,气温22°C"
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 服务端
python
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分别写了两个,具体融合的最后修改一下即可。
-
- STDIO客户端
python
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())
-
- SSE客户端
python
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上看
python
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的神秘面纱,见证科技如何让世界变得更加精彩。