如何使用MCP开发一个客户端和服务端
如何使用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_ServiceMCP_ServerAI_ModelUserExternal_ServiceMCP_ServerAI_ModelUser“查询北京天气”解析意图,生成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 服务端
  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的神秘面纱,见证科技如何让世界变得更加精彩。

如何使用MCP开发一个客户端和服务端的更多相关文章
- 使用GSoap开发WebService客户端与服务端
		
Gsoap 编译工具提供了一个SOAP/XML 关于C/C++ 语言的实现, 从而让C/C++语言开发web服务或客户端程序的工作变得轻松了很多. 用gsoap开发web service的大致思路 我 ...
 - python实现一个客户端与服务端的通信
		
函数介绍 Socket对象方法: 服务端: 函数 描述 .bind() 绑定地址关键字,AF_INET下以元组的形式表示地址.常用bind((host,port)) .listen() 监听TCP,可 ...
 - 二、网络编程-socket之TCP协议开发客户端和服务端通信
		
知识点:之前讲的udp协议传输数据是不安全的,不可靠不稳定的,tcp协议传输数据安全可靠,因为它们的通讯机制是不一样的.udp是用户数据报传输,也就是直接丢一个数据包给另外一个程序,就好比寄信给别人, ...
 - python网络编程TCP服务多客户端的服务端开发
		
#服务多客户端TCP服务端开发 2 #方法说明 3 """ 4 bind(host,port)表示绑定端口号,host是ip地址,ip地址一般不进 行绑定,表示本机的任何 ...
 - Java基础---Java---网络编程---TCP的传输、客户端和服务端的互访、建立一个文本转换器、编写一个聊天程序
		
演示TCP的传输的客户端和服务端的互访 需求:客户端给服务端发送数据,服务端收到后,给客户端反馈信息. 客户端: 1.建立Socket服务,指定要连接方朵和端口 2.获取Socket流中的输出流,将数 ...
 - SignalR 实现web浏览器客户端与服务端的推送功能
		
SignalR 是一个集成的客户端与服务器库,基于浏览器的客户端和基于 ASP.NET 的服务器组件可以借助它来进行双向多步对话. 换句话说,该对话可不受限制地进行单个无状态请求/响应数据交换:它将继 ...
 - Asp.Net MVC 模型验证详解-实现客户端、服务端双重验证
		
概要 在asp.net webform开发中经常会对用户提交输入的信息进行校验,一般为了安全起见大家都会在客户端进行Javascript(利于交互).服务端双重校验(安全).书写校验代码是一个繁琐的过 ...
 - Android BLE与终端通信(三)——客户端与服务端通信过程以及实现数据通信
		
Android BLE与终端通信(三)--客户端与服务端通信过程以及实现数据通信 前面的终究只是小知识点,上不了台面,也只能算是起到一个科普的作用,而同步到实际的开发上去,今天就来延续前两篇实现蓝牙主 ...
 - 从零讲解搭建一个NIO消息服务端
		
本文首发于本博客,如需转载,请申明出处. 假设 假设你已经了解并实现过了一些OIO消息服务端,并对异步消息服务端更有兴趣,那么本文或许能带你更好的入门,并了解JDK部分源码的关系流程,正如题目所说,笔 ...
 - Netty入门——客户端与服务端通信
		
Netty简介Netty是一个基于JAVA NIO 类库的异步通信框架,它的架构特点是:异步非阻塞.基于事件驱动.高性能.高可靠性和高可定制性.换句话说,Netty是一个NIO框架,使用它可以简单快速 ...
 
随机推荐
- 性能优化之使用HTTP2.0
			
HTTP2.0 的优势 配置 测试 怎么查看当前网站http版本 chrome HTTP协议版本检测 Chrome 控制台 window.chrome.loadTimes() { commitLoad ...
 - Ubuntu截屏工具推荐
			
Ubuntu截屏工具推荐 本篇博文推荐Ubuntu下的截屏工具Flameshot,可以作为Windows下Snipaste截图工具的平替. GitHub地址:https://github.com/fl ...
 - ORA-28001:口令已经失效
			
Oracle用户口令默认的有效期导致的一个异常,留爪. Oralce11G下,创建的用户及口令,也就是用户密码默认会有个180天的过期时间, 如果超过180天用户口令未做修改,则该用户口令失效,也就是 ...
 - frxpngimage单元的编译错误:frxpngimage left side cannot be assigned to
			
一个老项目中,使用软数字录入数据,编译时出现n个:frxpngimage left side cannot be assigned to 原因是使用了frxpngimage单元,在D11中编译出现若干 ...
 - 牛客小白月赛104 C-小红打怪
			
小红打怪 答案有单调性,使用二分答案来做 但是当时没有想到用二分,而是卡在怎么处理这三种攻击了. 可以把进行x回合的攻击,分为先进行x回合的全体打击,再进行x回合的范围打击,最后验证剩余血量够不够x回 ...
 - 微软正式发布 .NET 10 Preview 3
			
2025年4月11日,.NET团队在博客上宣布了.NET 10 Preview 3的正式发布,文章参见:https://devblogs.microsoft.com/dotnet/dotnet-10- ...
 - Tryhackme部分翻译学习
			
Tryhackme部分翻译学习 1.Weaponization WSH 上传txt到桌面 Set shell = WScript.CreateObject("Wscript.Shell&qu ...
 - Android编译时动态插入代码原理与实践
			
本文同步发布于公众号:移动开发那些事:Android编译时动态插入代码原理与实践 Android开发中,编译时动态插入代码是一种高效,并且对业务逻辑是低侵入性的方案,常用于增加通用的埋点能力,或者插入 ...
 - Laravel RCE(CVE-2021-3129)漏洞复现
			
Laravel框架简介 Laravel是一套简洁.优雅的PHP Web开发框架(PHP Web Framework).它可以让你从面条一样杂乱的代码中解脱出来:它可以帮你构建一个完美的网络APP,而且 ...
 - python中_自动生成的_pycache__文件夹
			
_pycache__文件夹可以看作该文件夹下文件已被python接管或者说编译过. 在第一次执行代码的时候,Python解释器已经把编译的字节码放在__pycache__文件夹中,这样以后再次运行的话 ...