【AI】开发笔记:一文入门MCP开发,架起LLM与业务系统桥梁

1、什么是MCP协议?

MCP(Model Context Protocol,模型上下文协议) 是由 Anthropic 于去年11月正式提出的一种开放标准,旨在通过统一的客户端-服务器架构解决 LLM 应用与数据源连接的难题,MCP 使得 AI 应用能够安全地访问和操作本地及远程数据,为 AI 应用提供了连接万物的接口,提升了智能体Agent的开发效率。截止目前,已上千种MCP工具诞生,在强悍的MCP生态加持下,人人手搓Manus的时代即将到来。

2、MCP技术架构

MCP 遵循客户端-服务器架构(client-server),其中包含以下几个核心概念:

  • MCP 主机(MCP Hosts):发起请求的 LLM 应用程序(例如 Claude Desktop、IDE 或 AI 工具)。
  • MCP 客户端(MCP Clients):在主机程序内部,与 MCP server 保持 1:1 的连接。
  • MCP 服务器(MCP Servers):为 MCP client 提供上下文、工具和 prompt 信息。
  • 本地资源(Local Resources):本地计算机中可供 MCP server 安全访问的资源(例如文件、数据库)。
  • 远程资源(Remote Resources):MCP server 可以连接到的远程资源(例如通过 API)。
    在这里插入图片描述

3、MCP Client 工作流程

  • MCP client 首先从 MCP server 获取可用的工具列表。
  • 将用户的查询连同工具描述通过 function calling 一起发送给 LLM。
  • LLM 决定是否需要使用工具以及使用哪些工具。
  • 如果需要使用工具,MCP client 会通过 MCP server 执行相应的工具调用。
  • 工具调用的结果会被发送回 LLM。
  • LLM 基于所有信息生成自然语言响应。
  • 最后将响应展示给用户。

4、MCP Server 关键组件

  • 工具(Tools):定义允许 LLM 自动调用的函数(需要用户批准),包括查询和写入操作。
  • 资源(Resources):定义 LLM 可以访问的只读数据源,为 LLM 提供上下文内容,如帮助文档、日志信息等。
  • 提示(Prompts):定义可重用的提示模板,帮助用更好的引导 LLM 以标准化的方式完成任务。

这些功能使 MCP server 能够为 AI 应用提供丰富的上下文信息和操作能力,从而增强 LLM 的实用性和灵活性。

5、MCP 通信方式

  • 标准输入输出(Standard Input/Output, stdio):客户端通过启动服务器子进程并使用标准输入(stdin)和标准输出(stdout)建立双向通信,一个服务器进程只能与启动它的客户端通信(1:1 关系)。stdio 适用于本地快速集成的场景,在本文中,我们将使用这种方式来编写 MCP 客户端。

  • 服务器发送事件(Server-Sent Events, SSE):是一种基于 HTTP 协议的技术,允许服务器向客户端单向、实时地推送数据。服务器作为独立进程运行,客户端和服务器代码完全解耦,支持多个客户端随时连接和断开。

6、 本地Server(STDIO)开发实例

6.1、server端开发

创建server代码:server_stdio.py

from mcp.server.fastmcp import FastMCP
from utils.date import get_time
from utils.weather import get_weather, format_weather

# 初始化 FastMCP 服务器,命名为 “MyMCPServer”
mcp = FastMCP("MyMCPServer")

# 定义工具函数,获取指定城市的天气
@mcp.tool()
async def fetch_weather(city: str) -> str:
    """
    输入指定城市的名称,返回今日天气查询结果。
    :param city: 城市名称(需使用中文)
    :return: 格式化后的天气信息
    """
    data = await get_weather(city)
    return format_weather(data)

# 定义工具函数,返回当前的日期和时间(ISO格式)
@mcp.tool()
def fetch_time():
    """返回当前的日期和时间。 """
    return get_time()

# 定义只读资源,返回当前项目的README.md文件内容
@mcp.resource("file://README.md")
def get_file() -> str:
    """Return the contents of README.md file"""
    with open("README.md", "r") as f:
        return f.read()

# 定义提示词模板,结合用户入参生成预置格式的提示词    
@mcp.prompt()
def role_prompt(role: str) -> str:
    """Create a prompt for role"""
    return f"""You are a '{role}' with 10-year experience and an elite expert with deep knowledge, you can helper user in your filed
"""

#运行服务器,使用标准输入输出传输
if __name__ == "__main__":
    mcp.run(transport='stdio')

utils/date.py:

import datetime

def get_timezone():
    """返回服务器的时区设置"""
    # 获取当前本地时间
    now = datetime.datetime.now()
    # 获取时区信息
    timezone = now.astimezone().tzinfo
    return(f"当前时区信息: {timezone}")

def get_time():
    """返回服务器的日期和时间,格式为ISO 8601。 """
    return datetime.datetime.now().isoformat()

#print(get_timezone())

utils/weather.py:

import json
import httpx
from typing import Dict, Any
import asyncio

async def get_weather(city: str):
    """返回指定城市的天气信息。"""
    weather_api_url = "https://shanhe.kim/api/za/tianqi.php"
    params = {
        "city": city,
        "type": "json"  # json text
    }

    async with httpx.AsyncClient() as client:
        try:
            response = await client.get(weather_api_url, params=params, timeout=30.0)
            response.raise_for_status()
            return response.json() # 返回字典类型
        except httpx.HTTPStatusError as e:
            return {"error": f"HTTP 错误: {e.response.status_code}"}
        except Exception as e:
            return {"error": f"请求失败: {str(e)}"}
        
def format_weather(data: dict[str, Any] | str) -> str:
    """
    将天气数据格式化为易读文本。
    :param data: 天气数据(可以是字典或 JSON 字符串)
    :return: 格式化后的天气信息字符串
    """
    # 如果传入的是字符串,则先转换为字典  
    if isinstance(data, str):
        try:
            data = json.loads(data)
        except Exception as e:
            return f"无法解析天气数据: {e}"
        
    # 如果数据中包含错误信息,直接返回错误提示
    if "error" in data:
        return f"⚠️ {data['error']}"
    
    # 提取数据时做容错处理
    #city = data.get("name", "未知")
    city = data.get("data", {}).get("city", "未知")
    #country = data.get("sys", {}).get("country", "未知")
    temp = data.get("data", {}).get("current", {}).get("temp", "N/A")
    humidity = data.get("data", {}).get("current", {}).get("humidity", "N/A")
    wind_speed = data.get("data", {}).get("current", {}).get("windSpeed", "N/A")
    # weather 可能为空列表,因此用 [0] 前先提供默认字典
    #weather_list = data.get("weather", [{}])
    #description = weather_list[0].get("description", "未知")
    weather = data['data']['weather']
    return (
        f"城市:{city},当前温度: {temp}°C,湿度: {humidity},风速: {wind_speed} m/s,天气: {weather}\n"
    )
    
"""
async def query_weather(city: str) -> str:
    data = await get_weather(city)
    return format_weather(data)

#测试代码
async def main():
    # 测试函数
    print("正在获取天气信息...")
    weather_info = await query_weather('杭州')
    print(weather_info)

asyncio.run(main())
"""

6.2、client端开发

创建client代码:client_stdio.py

import asyncio
from typing import Optional
from contextlib import AsyncExitStack
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
from dotenv import load_dotenv

import os
import json
from openai import OpenAI

# 加载 .env 文件,确保 API Key 受到保护
load_dotenv()

class MCPClient:
    def __init__(self):
        """初始化 MCP 客户端"""
        self.exit_stack = AsyncExitStack()
        self.session: Optional[ClientSession] = None
        self.exit_stack = AsyncExitStack()
        #self.anthropic = Anthropic()
        # 创建OpenAI client
        self.openai_api_key = os.getenv("DEEPSEEK_API_KEY") # 读取 OpenAI API Key
        self.base_url = os.getenv("BASE_URL") # 读取 BASE YRL
        self.model = os.getenv("MODEL") # 读取 model
        if not self.openai_api_key:
            raise ValueError("❌ 未找到 OpenAI API Key,请在 .env 文件中设置OPENAI_API_KEY")
        self.client = OpenAI(api_key=self.openai_api_key, base_url=self.base_url)
        
    async def connect_to_server(self, server_script_path: str):
        """连接到 MCP 服务器并列出可用工具
        Args:
            server_script_path: Path to the server script (.py or .js)
        """
        is_python = server_script_path.endswith('.py')
        is_js = server_script_path.endswith('.js')
        if not (is_python or is_js):
            raise ValueError("服务器脚本必须是 .py 或 .js 文件")
        command = "python" if is_python else "node"
        server_params = StdioServerParameters(
            command=command,
            args=[server_script_path],
            env=None
        )

        # 启动 MCP 服务器并建立通信
        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()
        
        # 列出 MCP 服务器上的工具
        response = await self.session.list_tools()
        tools = response.tools
        print("\n已连接到服务器,支持以下工具:", [tool.name for tool in tools])
    
    async def process_query(self, query: str) -> str:
        """
        使用大模型处理查询并调用可用的 MCP 工具
        """
        messages = [{"role": "user", "content": query}]
        response = await self.session.list_tools()
        available_tools = [{
            "type": "function",
            "function": {
                "name": tool.name,
                "description": tool.description,
                "input_schema": tool.inputSchema
            }
        } for tool in response.tools]

        response = self.client.chat.completions.create(
            model=self.model,
            messages=messages,
            tools=available_tools   # Function Calling
        )
        # 处理返回的内容
        content = response.choices[0]
        if content.finish_reason == "tool_calls":
            # 如何是需要使用工具,就解析工具
            tool_call = content.message.tool_calls[0]
            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"\n\n[Calling tool {tool_name} with args {tool_args}]\n\n")
            # 将模型返回的调用哪个工具数据和工具执行完成后的数据都存入messages中
            messages.append(content.message.model_dump())
            messages.append({
                "role": "tool",
                "content": result.content[0].text,
                "tool_call_id": tool_call.id,
            })
            # 将上面的结果再返回给大模型用于生产最终的结果
            response = self.client.chat.completions.create(
                model=self.model,
                messages=messages,
            )
            return response.choices[0].message.content
            
        return content.message.content
    
    async def chat_loop(self):
        """运行交互式聊天循环"""
        print("\n🤖 MCP 客户端已启动!输入 'quit' 退出")
        while True:
            try:
                query = input("\n你: ").strip()
                if query.lower() == 'quit':
                    break
                response = await self.process_query(query) # 发送用户输入到 OpenAI API
                print(f"\n🤖 OpenAI: {response}")
            except Exception as e:
                print(f"\n⚠️ 发生错误: {str(e)}")
            
    async def cleanup(self):
        """清理资源"""
        await self.exit_stack.aclose()

# main主入口
async def main():
    if len(sys.argv) < 2:
        print("Usage: python client.py <path_to_server_script>")
        sys.exit(1)

    client = MCPClient()
    try:
        await client.connect_to_server(sys.argv[1])
        await client.chat_loop()
    finally:
        await client.cleanup()

if __name__ == "__main__":
    import sys
    asyncio.run(main())

    # 启动客户端:uv run client_stdio.py server_stdio.py

6.3、自开发客户端测试

启动客户端,同时启动服务端子进程:

uv run client_stdio.py server_stdio.py

演示如下:

(mcp-demo) (base) root@ubuntu2204-dev:/data/Developer/Workspace/Python/MCP/mcp-demo# uv run client_stdio.py server_stdio.py
[04/12/25 01:20:13] INFO     Processing request of type ListToolsRequest                                                                            server.py:534

已连接到服务器,支持以下工具: ['fetch_weather', 'fetch_time']

🤖 MCP 客户端已启动!输入 'quit' 退出

你: 当前天气
[04/12/25 01:20:28] INFO     Processing request of type ListToolsRequest                                                                            server.py:534

🤖 OpenAI: 请告诉我您想查询哪个城市的天气(请使用中文城市名称),我可以为您提供最新的天气信息。

你: 杭州
[04/12/25 01:20:38] INFO     Processing request of type ListToolsRequest                                                                            server.py:534
[04/12/25 01:20:42] INFO     Processing request of type CallToolRequest                                                                             server.py:534
                    INFO     HTTP Request: GET https://shanhe.kim/api/za/tianqi.php?city=%E6%9D%AD%E5%B7%9E&type=json "HTTP/1.1 200 OK"           _client.py:1740


[Calling tool fetch_weather with args {'city': '杭州'}]



🤖 OpenAI: 杭州当前天气情况如下:

- **温度**:19.7°C  
- **湿度**:90%  
- **风速**:1级(约1米/秒)  
- **天气状况**:阴转中雨  

建议出行携带雨具,注意路面湿滑。如需其他信息,可随时告知!

你: 当前时间
[04/12/25 01:20:54] INFO     Processing request of type ListToolsRequest                                                                            server.py:534
[04/12/25 01:20:59] INFO     Processing request of type CallToolRequest                                                                             server.py:534


[Calling tool fetch_time with args {}]



🤖 OpenAI: 当前时间是 **2025年4月12日 01:20**(UTC+8)。如果需要更具体的时间或时区调整,请告诉我!

你: quit

6.4、Cherry Studio客户端测试:

6.4.1、添加本地 MCP 服务器:

在这里插入图片描述
参数设置如下:

--directory
D:\Workspace\JupyterSpace\MCP\mcp-demo
run
server_stdio.py

6.4.2、对话测试

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

7、远程 Server(SSE)开发实例

SSE 的主要特点包括:

  • 单向通信:服务器主动向客户端推送数据,客户端无法通过同一连接向服务器发送数据。
  • 基于 HTTP 协议:利用现有的 HTTP 协议,无需额外的协议支持,易于实现和部署。
  • 轻量级:实现简单,适用于需要实时更新的应用场景,如新闻推送、股票行情等。
  • 自动重连:客户端在连接断开时会自动尝试重新连接,确保数据传输的连续性。

7.1、server端开发

创建python文件:mcp-server-sse.py

from mcp.server.fastmcp import FastMCP
from datetime import datetime
from starlette.applications import Starlette
from starlette.routing import Route, Mount
from starlette.requests import Request
from starlette.responses import JSONResponse
from mcp.server.sse import SseServerTransport  # 假设 SseServerTransport 在此处
from mcp.server import Server
import argparse
import uvicorn
from utils.weather import get_weather, format_weather
from utils.date import get_time

# 初始化 FastMCP 服务器,命名为 “MyMCPServer”
mcp = FastMCP("MyMCPServer")

# 定义工具函数,获取指定城市的天气
@mcp.tool()
async def fetch_weather(city: str) -> str:
    """
    输入指定城市的名称,返回今日天气查询结果。
    :param city: 城市名称(需使用中文)
    :return: 格式化后的天气信息
    """
    data = await get_weather(city)
    return format_weather(data)

# 定义工具函数,返回当前的日期和时间(ISO格式)
@mcp.tool()
def fetch_time():
    """返回当前的日期和时间。 """
    return get_time()

async def hello(request):
    return JSONResponse({"message": "Welcome to MyMCPServer!"})

# 定义只读资源,返回当前项目的README.md文件内容
@mcp.resource("file://README.md")
def get_file() -> str:
    """Return the contents of README.md file"""
    with open("README.md", "r") as f:
        return f.read()

# 定义提示词模板,结合用户入参生成预置格式的提示词    
@mcp.prompt()
def role_prompt(role: str) -> str:
    """Create a prompt for role"""
    return f"""You are a '{role}' with 10-year experience and an elite expert with deep knowledge, you can helper user in your filed
"""

# 创建服务
def create_starlette_app(mcp_server: Server, *, debug: bool = False) -> Starlette:
    """Create a Starlette application that can server the provied mcp server with SSE."""
    sse = SseServerTransport("/messages/")

    async def handle_sse(request: Request) -> None:
        async with sse.connect_sse(
                request.scope,
                request.receive,
                request._send,
        ) as (read_stream, write_stream):
            await mcp_server.run(
                read_stream,
                write_stream,
                mcp_server.create_initialization_options(),
            )

    return Starlette(
        debug=debug,
        routes=[
            Route("/", endpoint=hello),
            Route("/sse", endpoint=handle_sse),
            Mount("/messages/", app=sse.handle_post_message),
        ],
    )

#运行服务器,使用sse传输
if __name__ == "__main__":
    mcp_server = mcp._mcp_server

    parser = argparse.ArgumentParser(description='Run MCP SSE-based server')
    parser.add_argument('--host', default='192.168.110.201', help='Host to bind to')
    parser.add_argument('--port', type=int, default=8501, help='Port to listen on')
    args = parser.parse_args()

    # Bind SSE request handling to MCP server
    starlette_app = create_starlette_app(mcp_server, debug=True)

    uvicorn.run(starlette_app, host=args.host, port=args.port)

    # 启动服务:python server_sse.py

7.2、client端开发

创建client代码:client_sse.py

import asyncio
import os
import json
from typing import Optional
from contextlib import AsyncExitStack
from openai import OpenAI
from dotenv import load_dotenv
from mcp import ClientSession, StdioServerParameters
from mcp.client.sse import sse_client

# 加载 .env 文件,确保 API Key 受到保护
load_dotenv()

class MCPClient:
    def __init__(self):
        """初始化 MCP 客户端"""
        self.exit_stack = AsyncExitStack()
        self.openai_api_key = os.getenv("DEEPSEEK_API_KEY") # 读取 OpenAI API Key
        self.base_url = os.getenv("BASE_URL") # 读取 BASE YRL
        self.model = os.getenv("MODEL") # 读取 model
        
        if not self.openai_api_key:
            raise ValueError("❌ 未找到 OpenAI API Key,请在 .env 文件中设置OPENAI_API_KEY")
        self.client = OpenAI(api_key=self.openai_api_key, base_url=self.base_url)
        
        # 创建OpenAI client
        self.session: Optional[ClientSession] = None
        self.exit_stack = AsyncExitStack()

    async def connect_to_sse_server(self, server_url: str):
        """Connect to an MCP server running with SSE transport"""
        # 创建 SSE 客户端连接上下文管理器
        self._streams_context = sse_client(url=server_url)
        # 异步初始化 SSE 连接,获取数据流对象
        streams = await self._streams_context.__aenter__()

        # 使用数据流创建 MCP 客户端会话上下文
        self._session_context = ClientSession(*streams)
        # 初始化客户端会话对象
        self.session: ClientSession = await self._session_context.__aenter__()

        # 执行 MCP 协议初始化握手
        await self.session.initialize()
        
        # 列出 MCP 服务器上的工具
        response = await self.session.list_tools()
        tools = response.tools
        print("\n已连接到服务器,支持以下工具:", [tool.name for tool in tools])
    
    async def process_query(self, query: str) -> str:
        """
        使用大模型处理查询并调用可用的 MCP 工具 (Function Calling)
        """
        messages = [{"role": "user", "content": query}]
        response = await self.session.list_tools()
        available_tools = [{
            "type": "function",
            "function": {
                "name": tool.name,
                "description": tool.description,
                "input_schema": tool.inputSchema
            }
        } for tool in response.tools]
        print(available_tools)

        response = self.client.chat.completions.create(
            model=self.model,
            messages=messages,
            tools=available_tools
        )
        # 处理返回的内容
        content = response.choices[0]
        if content.finish_reason == "tool_calls":
            # 如何是需要使用工具,就解析工具
            tool_call = content.message.tool_calls[0]
            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"\n\n[Calling tool {tool_name} with args {tool_args}]\n\n")
            # 将模型返回的调用哪个工具数据和工具执行完成后的数据都存入messages中
            messages.append(content.message.model_dump())
            messages.append({
                "role": "tool",
                "content": result.content[0].text,
                "tool_call_id": tool_call.id,
            })
            # 将上面的结果再返回给大模型用于生产最终的结果
            response = self.client.chat.completions.create(
                model=self.model,
                messages=messages,
            )
            return response.choices[0].message.content
            
        return content.message.content
    
    async def chat_loop(self):
        """运行交互式聊天循环"""
        print("\n🤖 MCP 客户端已启动!输入 'quit' 退出")
        while True:
            try:
                query = input("\n你: ").strip()
                if query.lower() == 'quit':
                    break
                response = await self.process_query(query) # 发送用户输入到 OpenAI API
                print(f"\n🤖 OpenAI: {response}")
            except Exception as e:
                print(f"\n⚠️ 发生错误: {str(e)}")
            
    async def cleanup(self):
        """清理资源"""
        await self.exit_stack.aclose()

async def main():
    if len(sys.argv) < 2:
        print("Usage: python client.py <path_to_server_script>")
        sys.exit(1)
    client = MCPClient()
    try:
        await client.connect_to_sse_server(sys.argv[1])
        await client.chat_loop()
    finally:
        await client.cleanup()

if __name__ == "__main__":
    import sys
    asyncio.run(main())

    # 启动客户端:uv run client_sse.py http://192.168.110.201:8501/sse

7.3、自开发客户端测试

7.3.1、首先启动Linux服务端

python server_sse.py

显示如下:

(mcp-demo) (base) root@ubuntu2204-dev:/data/Developer/Workspace/Python/MCP/mcp-demo# python server_sse.py
INFO:     Started server process [924723]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://192.168.110.201:8501 (Press CTRL+C to quit)

在这里插入图片描述

7.3.2、然后启动Windows本地客户端

需指定服务端url:

uv run client_sse.py http://192.168.110.201:8501/sse

测试过程如下:

(mcp-demo) PS D:\Workspace\JupyterSpace\MCP\mcp-demo> uv run client_sse.py http://192.168.110.201:8501/sse

已连接到服务器,支持以下工具: ['fetch_weather', 'fetch_time']

🤖 MCP 客户端已启动!输入 'quit' 退出

你: 当前时间
[{'type': 'function', 'function': {'name': 'fetch_weather', 'description': '\n    输入指定城市的名称,返回今日天气查询结果。\n    :param city: 城市名称(需使用 中文)\n    :return: 格式化后的天气信息\n    ', 'input_schema': {'properties': {'city': {'title': 'City', 'type': 'string'}}, 'required': ['city'], 'title': 'fetch_weatherArguments', 'type': 'object'}}}, {'type': 'function', 'function': {'name': 'fetch_time', 'description': '返回当前的日期和时间。 ', 'input_schema': {'properties': {}, 'title': 'fetch_timeArguments', 'type': 'object'}}}]


[Calling tool fetch_time with args {}]



🤖 OpenAI: 当前时间是2025年4月12日,凌晨1点05分(UTC时间)。如果需要转换为您的时区,请告诉我您所在的城市或时区。

你: 杭州天气
[{'type': 'function', 'function': {'name': 'fetch_weather', 'description': '\n    输入指定城市的名称,返回今日天气查询结果。\n    :param city: 城市名称(需使用 中文)\n    :return: 格式化后的天气信息\n    ', 'input_schema': {'properties': {'city': {'title': 'City', 'type': 'string'}}, 'required': ['city'], 'title': 'fetch_weatherArguments', 'type': 'object'}}}, {'type': 'function', 'function': {'name': 'fetch_time', 'description': '返回当前的日期和时间。 ', 'input_schema': {'properties': {}, 'title': 'fetch_timeArguments', 'type': 'object'}}}]


[Calling tool fetch_weather with args {'city': '杭州'}]



🤖 OpenAI: 杭州当前天气为阴转中雨,气温19.9°C,湿度89%,风速1级(约1米/秒)。出行建议携带雨具,注意路面湿滑。

你: quit

7.4、Cherry Studio 客户端测试:

7.4.1、添加远程 MCP 服务器:

在这里插入图片描述

7.4.2、对话测试:

效果同上。

8、总结

参考

[1]: Model Context Protocol 官方文档
[2]: MCP Client 开发文档

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值