如何使用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. 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. 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开发一个客户端和服务端的更多相关文章

  1. 使用GSoap开发WebService客户端与服务端

    Gsoap 编译工具提供了一个SOAP/XML 关于C/C++ 语言的实现, 从而让C/C++语言开发web服务或客户端程序的工作变得轻松了很多. 用gsoap开发web service的大致思路 我 ...

  2. python实现一个客户端与服务端的通信

    函数介绍 Socket对象方法: 服务端: 函数 描述 .bind() 绑定地址关键字,AF_INET下以元组的形式表示地址.常用bind((host,port)) .listen() 监听TCP,可 ...

  3. 二、网络编程-socket之TCP协议开发客户端和服务端通信

    知识点:之前讲的udp协议传输数据是不安全的,不可靠不稳定的,tcp协议传输数据安全可靠,因为它们的通讯机制是不一样的.udp是用户数据报传输,也就是直接丢一个数据包给另外一个程序,就好比寄信给别人, ...

  4. python网络编程TCP服务多客户端的服务端开发

    #服务多客户端TCP服务端开发 2 #方法说明 3 """ 4 bind(host,port)表示绑定端口号,host是ip地址,ip地址一般不进 行绑定,表示本机的任何 ...

  5. Java基础---Java---网络编程---TCP的传输、客户端和服务端的互访、建立一个文本转换器、编写一个聊天程序

    演示TCP的传输的客户端和服务端的互访 需求:客户端给服务端发送数据,服务端收到后,给客户端反馈信息. 客户端: 1.建立Socket服务,指定要连接方朵和端口 2.获取Socket流中的输出流,将数 ...

  6. SignalR 实现web浏览器客户端与服务端的推送功能

    SignalR 是一个集成的客户端与服务器库,基于浏览器的客户端和基于 ASP.NET 的服务器组件可以借助它来进行双向多步对话. 换句话说,该对话可不受限制地进行单个无状态请求/响应数据交换:它将继 ...

  7. Asp.Net MVC 模型验证详解-实现客户端、服务端双重验证

    概要 在asp.net webform开发中经常会对用户提交输入的信息进行校验,一般为了安全起见大家都会在客户端进行Javascript(利于交互).服务端双重校验(安全).书写校验代码是一个繁琐的过 ...

  8. Android BLE与终端通信(三)——客户端与服务端通信过程以及实现数据通信

    Android BLE与终端通信(三)--客户端与服务端通信过程以及实现数据通信 前面的终究只是小知识点,上不了台面,也只能算是起到一个科普的作用,而同步到实际的开发上去,今天就来延续前两篇实现蓝牙主 ...

  9. 从零讲解搭建一个NIO消息服务端

    本文首发于本博客,如需转载,请申明出处. 假设 假设你已经了解并实现过了一些OIO消息服务端,并对异步消息服务端更有兴趣,那么本文或许能带你更好的入门,并了解JDK部分源码的关系流程,正如题目所说,笔 ...

  10. Netty入门——客户端与服务端通信

    Netty简介Netty是一个基于JAVA NIO 类库的异步通信框架,它的架构特点是:异步非阻塞.基于事件驱动.高性能.高可靠性和高可定制性.换句话说,Netty是一个NIO框架,使用它可以简单快速 ...

随机推荐

  1. 记录一下 简单udp和sni 代理 done

    由于之前借鉴 Kestrel 了非常多抽象和优化实现,对于后续的扩展非常便利, 实现 简单udp和sni 代理 两个功能比预期快了超多(当然也有偷懒因素) (PS 大家有空的话,能否在 GitHub ...

  2. 【BUG】鸿蒙模拟器虚拟化问题的解决方案

    创建记事本文档.txt,键入以下代码: pushd "%~dp0" dir /b %SystemRoot%\servicing\Packages\*Hyper-V*.mum > ...

  3. python list 差集

    前言 有时候我们希望基于list得到一个集合C,该集合C的元素可以被描述为元素在集合A中而不在集合B中.即:差集. 基于set A = [1, 2, 3] B = [2, 3, 4] C = set( ...

  4. 安装调用.so文件

    博客地址:https://www.cnblogs.com/zylyehuo/ 使用 pwd 命令找到 .so 文件 首先使用 pwd 命令找到要安装的 .so 文件.通过使用此命令打印当前工作目录来找 ...

  5. osmts:OERV之一站式管理测试脚本

      最近团队里面实习的小伙伴开发了一个新的项目,可以用来一键式运行各种测试脚本并且完成数据总结,我也尝试部署了一下,遇到了一些问题,接下来一起解析一下这个项目.   首先是获取osmts git cl ...

  6. MySQL-InnoDB行锁

    InnoDB的锁类型 InnoDB存储引擎支持行锁,锁类型有两种: 共享锁(S锁) 排他锁(X锁) S和S不互斥,其他均互斥. 除了这两种锁以外,innodb还支持一种锁,叫做意向锁. 那么什么是意向 ...

  7. 面试题-MyBatis框架

    前言 MyBatis框架部分的题目,是我根据Java Guide的面试突击版本V3.0再整理出来的,其中,我选择了一些比较重要的问题,并重新做出相应回答,并添加了一些比较重要的问题,希望对大家起到一定 ...

  8. 【JDBC第2章】获取数据库连接

    第2章:获取数据库连接 2.1 要素一:Driver接口实现类 2.1.1 Driver接口介绍 java.sql.Driver 接口是所有 JDBC 驱动程序需要实现的接口.这个接口是提供给数据库厂 ...

  9. 【Linux】3.7 定时任务调度

    3.7定时任务调度 1. 任务调度原理 crond任务调度:crontab进行定时任务调度 使用方法:crontab [选项] crontab [选项] -e:编辑crontab定时任务 -i:查询c ...

  10. 【Linux】3.6 组管理和权限管理

    组管理和权限管理 1. Linux组基本介绍 Linux中每个用户属于一个组,不能独立于组以外.所以在Linux中每个文件存在组的概念: 所有者 所在组 其他组 改变用户所在组 2. 文件/目录所有者 ...